본문 바로가기
Tech Development/Python Backend (Flask API)

Python Backend - Study Notes 10

by JK from Korea 2023. 1. 1.

날짜: 2022.09.30

 

[Unit Test]

Unit Test는 우리가 만든 시스템이 정상작동하는지 확인해보는 절차이자 중요한 단계이다. 테스트가 왜 중요한지 간단하게 알아보고 Unit Test 구현을 바로 해보자.

 

[Why Unit Test?]

현재 구현중인 API 서버 등의 ‘시스템'을 테스트할 때 가장 중요한 것은 자동화이다. Manual Testing은 시간이 오래 걸리고 느리다. 테스트 자동화를 통해서 다음 3가지 요소를 갖추고 있어야된다.

 

  1. Repetitive
  2. Frequent
  3. Accurate

 

테스트의 종류는 크게 3가지로 나눌 수 있는데 다음과 같다.

 

  1. UI Test / End-To-End Test
  2. Integration Test
  3. Unit Test

 

UI Test는 사용자가 실제로 사용할 시스템과 가장 비슷한 환경을 테스트할 수 있는 장점이 있지만 프론트엔드부터 백엔드까지 모든 시스템을 실행시키고 연결해서 진행하기 때문에 자동화시키기 까다롭다. 그 다음으로 Integration Test는 지금까지 우리가 Miniter API를 개발하면서 했던 것들이다. API 서버를 로컬에서 실행시킨 뒤, 터미널에서 테스트용 HTTP 요청을 로컬에서 실행되고 있는 API 서버에 전송하여 올바른 HTTP 응답이 리턴되는지 확인하는 방식이다.

 

위의 2가지 테스트는 시스템 단위를 테스트하지만, Unit Test는 코드를 직접 테스트하는 것에 더 가깝다. 앞으로 우리는 코드로 코드를 테스트할 것이다. Unit Test는 실행이 쉽고 속도가 빠르다. 그리고 디버깅하기 비교적 수월하다. 하지만 시스템이 아니라 함수 단위로 쪼개서 테스트하다보니까 전체적 시스템의 정상 작동을 확인 하기에는 어렵다.

[테스트 피라미드]

[Unit Test via PyTest]

우리는 파이썬용 외부 라이브러리인 PyTest를 사용해서 Unit Test를 구현할 것이다. PyTest를 사용하기에 앞서 주의점이 있다.

 

PyTest는 파일 이름과 Unit Test 함수 이름 앞부분에 “test_”라고 되어있는 파일과 함수들만 테스트용으로 인식하고 실행한다. (ex. test_example.py, def test_multiply_by_two ())

일단 PyTest를 설치한다.

 

그리고 간단한 테스트런을 돌려본다.

 

[테스트 파일]

 : 이 라인부터 Unit Test 코드이자 함수이다. 파일 이름뿐만 아니라 함수 이름 앞에도 “test_”가 붙을 것을 볼 수 있다.

⑦ : multiply_by_two(4)의 결과가 7을 리턴하는지 테스트한다. 함수에 따르면 8이 올바른 답이므로 pytest 실행시 밑에 사진과 같이 Assertion Error가 뜬다.

[터미널에서 돌린 pytest의 결과]

“FAILURES”와 “Assertion Error”를 통해 어느 부분이 문제인지 알려주기 때문에 디버깅하기 수월하다. 에러가 없도록 수정해보자.

[올바른 리턴값으로 수정]
[굿!]

[Miniter API Unit Test]

미니터 API에 적용할 Unit Test를 만들어보자. 다시 한번 말하지만 unit test의 개념은 함수로 함수를 테스트하는 것이다. 우리가 테스트하는 단위가 “함수"임을 명심해야된다. 지금껏 구현한 엔드포인트는 함수 단위이다. 그렇기 때문에 각각의 엔드포인트를 테스트하는 unit test 파일을 만들것이다. 앞으로는 테스트용 디렉터리에서 작업할 것이다.

 

엔드포인트 테스트용 파일인 “test_endpoints.py”를 생성하자. 그리고 실제 API와 동일하게 데이터베이스도 필요하다.

 

[config.py에 테스트용 데이터베이스 정보를 넣어준다]

그리고 miniter 데이터베이스를 만들어준것 처럼 테스트용 데이터베이스인 “test_db”를 생성할 것이다. “test_db”도 MySQL RDB 타입이다.

[이미 만들어놓은 MySQL 계정으로 접속]
[테스트용 데이터베이스 만들기. 기존에 만든 데이터베이스와 동일함]

이미 만든 데이터베이스와 동일하기 때문에 익숙할 것이다. 다시 test_endpoints.py로 돌아와서 기존 app.py 처럼 데이터베이스와 연결하는 작업을 할 것이다.

[config.py에서 이미 ‘database’ 키 값을 ‘test_db’로 바꾸었기 때문에 테스트용 DB와 연결된다]

이제 Unit Test를 구현하기 위한 준비를 모두 마쳤다. PyTest는 기본적으로 “assert” 구문을 통해서 함수를 실행시키고 리턴값이 테스트 값과 동일한지 확인한다. 하지만 API 엔드포인트는 위에서 다룬 예시와 구조가 다르다. 엔드포인트는 서버가 실행되고 있다는 가정하에 HTTP 통신을 통해서 테스트한다. 하지만 Unit Test는 시스템을 실행시키지 않고 개별 함수만 접근한다.

어떻게 서버를 실행시키지 않고 서버 속 기능을 테스트하라는거지?

우리는 Flask의 “test_client” 함수를 사용해서 HTTP 통신을 ‘흉내'낼 것이다. 실제 HTTP 전송이 아니고 ‘test_client’를 통한 메모리상에서의 테스트이다. 이미 만들어놓은 API를 테스트하기 위함이므로 API 기능들은 app.py에서 가져와서 테스트 할 것이다.

 

⑥ : 인증 엔드포인트를 구현할 때 만든 “def create_app(test_config = None)”을 불러오는 것이다. API의 모든 엔드포인트를 담고 있는 함수이다.

 

⑩ : Decorator의 한 종류이다. @pytest.fixture 라는 decorator의 이름을 보면 우리가 사용하는 unit test 라이브러리인 pytest용 decorator임을 알 수 있다. Decorator의 역할은 밑에서 자세히 설명하겠다.

 

⑪ : “@pytest.fixture” decorator가 적용된 함수이다. fixture decorator가 적용된 함수는 이름이 중요하다. PyTest가 테스트할 함수를 찾을 때, 찾은 함수가 “api”처럼 테스트하려는 함수의 이름을 찾기 때문이다.

 

⑫ : import해서 사용중인 create_app 객체 (= 실제 miniter 객체)를 만들 때 “config.test_config”을 인자로 넘겨줌으로써 데이터베이스 설정이 모두 테스트용으로 바뀐다.

 

⑬ : TEST 옵션을 True로 설정하면 HTTP Request로 인한 Flask 에러가 생길 경우 불필요한 오류 메세지를 출력하지 않는다.

 

⑮ : 위에서 만든 create_app 인스턴스인 app는 우리가 이미 만든 실제 miniter API의 Flask 객체이다. 그러므로 Flask가 제공하는 함수인 test_client를 사용할 수 있다. 즉, test_client 함수를 호출해서 테스트용 클라이언트를 생성한다. 위에서 설명했듯이 test_client 함수는 “URI”를 인자로 받아서 URI를 기반으로 엔드포인트를 호출하고 데이터도 전송할 수 있다. 즉, HTTP를 기반으로 한 터미널 통신이 없지만, 마치 통신을 하고, 정보를 넘기고, 데이터베이스를 사용하는 등의 효과를 볼 수 있다.

 

여기까지의 구현이 중요한 이유는 “def api()”는 test 내내 사용되는 공통분모이기 때문이다. 보다시피 def api()에서 create_app를 하고 test_client를 활성화한다. 이 두 가지의 진짜 의미를 해석하면 다음과 같다.

 

  1. create_app :  우리가 만든 miniter API를 가져온다. 즉, test용 파일과 실제 miniter API의 연결고리 역할이다.
  2. test_client : HTTP 통신 기능을 재현한다.

 

이제 “def api()”를 통해서 miniter API를 테스트하는 함수를 만들 것이다. 가장 쉬운 ping 부터 하자.

 

[ping 엔드포인트 테스트 함수]

⑲ : “test_”로 함수 이름을 시작해야되는 것을 주의하자. 보다시피 “api”를 인자로 받는다. 인자를 받는다고? 예시로 다뤘던 multiply_by_two()는 인자가 없었다. 그리고 PyTest를 실행할때는 터미널에서 직접 실행하기 때문에 별도의 인수를 넘기지 못하는 구조이다. 근데 test_ping은 “api” 인자를 받는다. 이게 가능한 이유는 PyTest가 자동으로 “test_ping(api)”을 실행시킴과 동시에 인자 “api”를 인식하고, “api”와 동일한 이름을 가진 함수 중에 “@pytest.fixture” decorator가 적용된 함수를 찾아서 리턴 값을 인자로 넘겨주기 때문이다. 순서를 정리하면 다음과 같다.

 

터미널에 ‘PyTest’ 실행 → “test_” 이름을 가진 “test_ping(api)” 인식 → 함수 실행 → “api” 인자와 동일한 이름의 함수를 찾음 → fixture decorator가 있는지 확인 → “def api()”의 리턴값인 api”를 test_ping 인자 값으로 넘겨줌

 

⑳ : test_client의 get 메서드를 통해 가상의 GET 요청을 “/ping” URI와 연결된 엔드포인트로 전송한다.

㉑ :  HTTP Response 값의 “body” 부분에 “ping” 텍스트가 포함되어 있는지 확인한다. “b”는 string 타입을 byte로 변환시킨다. response.data는 byte 타입이다.

 

이제 터미널에서 pytest를 실행시키자.

 

[처음 실행 시켰을 때 에러가 떴다]

에러가 뜬 이유는 “test_endpoints.py”에 import pytest”를 하지 않았었기 때문이다. 위에 첨부된 사진을 보면 import가 되어있지만 테스트를 처음 돌릴때까지만 해도 하지 않은 상태였다. (책에서 import 안되어있었는데..하..)

 

PyTest를 테스트하는 파일에 따로 import를 해야지 “@pytest.fixture”를 가져다 쓸 수 있다. API 프로그래밍할때 “from flask import Flask”를 했던 것을 기억할 것이다. Flask를 import 했기 때문에 “@route” decorator를 사용할 수 있던 것이다. pytest도 같은 원리라고 생각하면 된다.

 

[다시 실행했더니..!]

이제 tweet 엔드포인트 테스트를 구현할 것이다. Tweet 엔드포인트는 밑의 4 가지 조건이 충족되어야된다.

  1. 사용자가 있다.
  2. 회원가입을 했다.
  3. 로그인을 한 상태이다.
  4. Tweet을 해야된다.

*test_endpoints.py 에 덧붙여서 작성한 코드이다.

 

Line 27~40 : tweets을 보낼 테스트 사용자를 먼저 생성한다. sign_up 엔드포인트에 new_user 정보를 넘겨주고, sign_up 엔드포인트가 json 정보를 소화하는지 status code로 확인한다. 이로서 sign_up 엔드포인트 테스트 완료!

(참고: jsonloads vs jsondumps)

 

Line 42~44 : sign_up 엔드포인트를 호출하면 “test_db”에 insert_user(), get_user()를 통해서 사용자 정보를 저장하고 다시 get해서 response.data로 넘겨준다. id의 경우 데이터베이스가 지정해서 넘겨주기 때문에 우리가 굳이 조작하지 않는다. 그리고 받은 id는 이후에 timeline 엔드포인트 테스트할 때 사용할 것이다.

 

Line 47~54 : Tweet 엔드포인트를 사용하기 위해서는 로그인을 해야된다. 이 과정에서 access token을 받아서 저장한다.

 

Line 56~63 : Access token을 ‘Authorization’헤더에 첨부해서 tweet request를 보낸다.

 

Line 65~78 : Tweet이 정상적으로 저장되었는지 확인하기 위해서 status code와 timeline으로 확인한다.

 

[에러가 떴다..]

Duplicate entry ... key ‘email’ 에러를 받았는데 이전 포스트에서 봤던 에러와 동일하다. 이메일은 UNIQUE KEY 이기 때문에 동일한 이메일로 사용자를 만들려고 하면 에러가 발생한다. 혹시나 하는 마음에서 miniter 데이터베이스를 확인했더니..

 

[현재 miniter 데이터베이스]

“test_db” 데이터베이스에 저장되어야되는 정보가 “miniter” 데이터베이스로 가고 있다. 다음과 같이 API와 데이터베이스를 디버깅하기 시작했다.

 

  1. Miniter 데이터베이스의 세부 정보 삭제. users, tweets, users_follow_list 모두 Empty Set으로 초기화시켰다. (초기화할 때 “delete from ;” 구문을 사용했는데 이 경우 외부 키가 걸려있는 테이블의 경우, 외부 키가 있는 테이블의 정보부터 삭제해야된다. 아니면 ERROR 1451 (23000)이 발생한다.)
  2.  

app.py의 create_app 함수를 수정한다. test_config을 인자로 넘겨주니까 “None”이 아니라 “test_db” 데이터베이스로 정보가 저장되도록 한다.

일단 pytest는 통과.. “test_tweet” 함수는 문제가 없다. 혹시 모르니까 데이터베이스 확인.

 

  1.  

다시 miniter 데이터베이스로 저장되었다.. 하..

 

  1. 문제는 config.py 에 있었다. ‘DB_URL’ 설정에서 데이터베이스 정보를 모두 miniter로 설정하고 바꾸지 않았다. 바꾸면 다음과 같다.

[‘DB_URL’에서 “db → test_db” 로 재설정]

  1. 다시 테스트!

[성공!]

  1. 데이터베이스 확인!

[굿!]

Tweet 엔드포인트에 대한 Unit Test를 실행하기 위해서는 여러개의 사전 단계들을 거쳐야했다. 사용자 생성, 로그인, 인증 등.. 이런 정보는 다른 엔드포인트를 테스트할때도 사용해야되는 중복 정보이다. 하지만 각각의 Unit Test는 독립적이어야한다. 그래서 테스트를 할 때마다 데이터를 지우고 만들어야하는데 이 과정을 수월하게 하기 위해서 테스트마다 실행되는 setup function과 teardown function을 미리 구현해서 데이터베이스 관리를 자동으로 하게끔 만들것이다.

“set-up function → main 엔드포인트 테스트 → teardown function

[set-function]

  1. sign_up 엔드포인트를 호출해서 사용자를 생성하는 대신에 “setup_function()”을 통해 “test_db” 데이터베이스에 미리 사용자 정보와 “id”까지 지정해서 기입해줄 것이다.
  2. timeline 엔드포인트에서 tweets을 불러올때 “id” 정보를 미리 지정한 숫자로 사용할 수 있기 때문에 데이터베이스에서 만들어주는 번호가 아니라 우리가 직접 1과 2로 지정한다.

[teardown function]

  1. TRUNCATE SQL 구문은 해당 테이블 데이터를 모두 삭제한다.
  2. SET FOREIGN_KEY_CHECKS는 지정 테이블이 외부 테이블과 외부 키로 연결되어있을 경우 데이터 삭제가 안되기 때문에 연결고리를 풀어주는 것이다.

전체 코드 링크 : old link

→ vscode로 전환하면서 많은 것을 바꿨다. 다음 포스트 참고할 것.

위의 링크로 들어가면 구름 IDE 워크스페이스로 연결될것이다. 구름 IDE가 다 좋은데 Github와 연결하는 것이 까다로워서 아직 못했다. 좀 치명적인 단점이라서 아마 다른 IDE 를 앞으로 더 사용할 것 같다. 암튼, 링크를 따라 들어가면 위의 사진들에 해당하는 코드는 없을 수도 있다. 이후 포스트에서 레이어드 아키텍처로 리모델링 들어가면 구조가 바뀌기 때문이다.

728x90
반응형

댓글