앞 단계 참조 링크:
- 상태 코드: 2024.03.26 - [Web 개발/FAST API (인프런 강의 내용)] - 1 FastAPI 알아보기
- 프로젝트 소개 및 환경 구축: 2024.04.05 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습1 GET API 전체조회
- 1 실습1 GET API ToDo 전체 조회: 2024.04.05 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습1 GET API 전체조회
- 1 실습2 GET API ToDo 단일 조회: 2024.04.09 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습2 GET API 단일조회
- 1 실습3 POST API ToDo 생성: 2024.04.15 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습3 POST API todo 생성
- 1 실습4 PATCH API ToDo 수정: 2024.04.16 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습4 PATCH API todo 수정
- 1 실습5 DELETE API ToDo 삭제: 2024.04.17 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습5 DELETE API todo 삭제
- 1 실습6 ERROR 처리: 2024.04.18 - [Web 개발/FAST API (인프런 강의 내용)] - 1 실습6 ERROR 처리
- 2 데이터베이스: 2024.04.24 - [Web 개발/FAST API (인프런 강의 내용)] - 2 데이터베이스
- 2 실습1 MySQL 컨테이너 실행 (docker): 2024.04.24 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습1 MySQL 컨테이너 실행 (docker)
- 2 실습2 MySQL 접속 및 사용: 2024.04.25 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습2 MySQL 접속 및 사용
- 2 실습3 데이터베이스 연결: 2024.04.25 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습3 데이터베이스 연결
- 2 실습4 ORM 모델링: 2024.05.02 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습4 ORM 모델링
- 2 실습5 ORM GET 전체조회 API: 2024.05.03 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습5 ORM 적용 - GET 전체조회 API
- 2 실습6 ORM HTTP Response 처리: 2024.05.10 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습6 ORM 적용 - HTTP Response 처리
- 2 실습7 ORM GET 단일조회 API: 2024.05.14 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습7 ORM 적용 - GET 단일조회 API
- 2 실습8 ORM Refactoring: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습8 ORM 적용 - Refactoring
- 2 실습9 ORM POST API: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습9 ORM 적용 - POST API
- 2 실습10 ORM PATCH API: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습10 ORM 적용 - PATCH API
- 2 실습11 ORM DELETE API: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 2 실습11 ORM 적용 - DELETE API
- 3 테스트 코드 PyTest: 2024.06.01 - [Web 개발/FAST API (인프런 강의 내용)] - 3 테스트 코드 PyTest
- 3 실습1 PyTest 세팅: 2024.07.22 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습1 PyTest 세팅
- 3 실습2 테스트 코드 GET 전체조회 API: 2024.08.01 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습2 테스트 코드 - GET 전체조회 API
00 개요
- 목적: GET 전체조회 API에 대한 테스트 코드에 mocking 기술 적용하기
- 현재 테스트 코드의 문제점: API 요청을 여러번 하고 있음
- test_main.py의 test_get_todos() 함수를 보면, 우리가 작성한 코드에서는 해당 API를 한 번 요청할 때마다 DB를 한 번 조회함
- 지금은 조회만 하는 것이기에 문제가 없지만 데이터를 생성/변경/삭제하는 경우에는 실제 DB에 영향을 주게됨 (문제 발생)
- 해결 방식으로는 테스트 코드를 위한 테스트 DB를 따로 만들어서 테스트 전 docker를 띄웠다가 테스트가 끝나면 docker를 내려서 데이터 관리하는 방식이 있음
- 그러나 현재 프로젝트에서 보면 DB와 통신하는 부분 자체가 오래 걸리는 연산임 → 문제!
- 그러므로 mocking 기술을 이용하여 실제 DB에 요청을 하지 않지만 마치 DB에 요청을 하는 것처럼 속이는 기술을 적용할 것
- mocking을 반드시 사용해야하는 것은 아님
- mocking을 사용하는 사례:
- 오래 걸리는 작업 (예, 외부 API를 이용한다거나 DB 요청을 한다거나 등)이 있을 경우 그 부분에 mocking을 사용하여 대체하면 실제로 그 부분이 동작을 하지 않아서 테스트 코드가 훨씬 빨리 동작하게 됨
- (mocking을 사용하지 않고 매번 테스트마다 실제로 DB를 조회하는 경우 데이터가 대용량인 경우 테스트 코드 동작 시간이 오래 걸릴 수 있음 (5분 ~ 1시간)
- 현재 테스트 코드의 목적이 실제 데이터를 검증하는 것은 아니기에 mocking 기술을 적용해볼 것
- 오래 걸리는 작업 (예, 외부 API를 이용한다거나 DB 요청을 한다거나 등)이 있을 경우 그 부분에 mocking을 사용하여 대체하면 실제로 그 부분이 동작을 하지 않아서 테스트 코드가 훨씬 빨리 동작하게 됨
01 mocking 적용하기
1. 필요한 패키지 설치: pytest-mock
- pytest-mock: pytest에서 mocking이라는 기술을 사용하기 위한 라이브러리
- 하는 김에 pip도 업그레이드 해줌
$ pip install --upgrade pytest=-mock
2. test_main.py 수정
- 다음과 같이 작성:
# /c/Users/관리자/Desktop/projects/todos/src/tests/test_main.py 내용
from fastapi.testclient import TestClient
from src.main import app
from src.database.orm import ToDo # 추가
client = TestClient(app=app)
def test_health_check():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"ping": "pong"}
def test_get_todos(mocker): # 추가 - mocker 인자 추가
# order = ASC
mocker.patch("main.get_todos", return_value=[ # 추가
ToDo(id=1, contents="FastAPI Section 0", is_done=True), # 추가
ToDo(id=2, contents="FastAPI Section 2", is_done=False), # 추가
]) # 추가
response = client.get("/todos")
assert response.status_code == 200
assert response.json() == {
"todos": [
{"id": 1, "contents": "FastAPI Section 0", "is_done": True},
{"id": 2, "contents": "FastAPI Section 2", "is_done": False}, # True -> False 수정 (mocker의 내용과 동일하게끔)
# {"id": 3, "contents": "FastAPI Section 3", "is_done": True}, # 제거
]
}
# order = DESC
response = client.get("/todos?order=DESC")
assert response.status_code == 200
assert response.json() == {
"todos": [
# {"id":3, "contents": "FastAPI Section 3", "is_done": True}, # 제거
{"id":2, "contents": "FastAPI Section 2", "is_done": False}, # True -> False 수정 (mocker의 내용과 동일하게끔)
{"id":1, "contents": "FastAPI Section 0", "is_done": True},
]
}
- 설명
- from src.database.orm import ToDo
- response 값에 사용될 우리가 만든 ToDo 클래스 참조
- def test_get_todos(mocker):
- mocking이 필요한 부분에 mocker라는 인자를 주입하여 mockig 기술 사용 가능
- mocker.patch("main.get_todos", return_value=[
- ToDo(id=1, contents=”FastAPI Section 0”, is_done=True),
- ToDo(id=2, contents=”FastAPI Section 1”, is_done=False),
- ])
- 실제 코드가 동작하기 전 (i.e., API를 호출하기 전)에 mocking을 적용해줘야 함
- mocker.patch()라는 메소드 사용하는 것
- mocking 하려고 하는 부분이 main.py에 보면
- def get_todos_handler() 안에
- todos: List[ToDo] = get_todos(session=session)
- 여기서 우리의 목적은 get_todos() 함수 자체를 mocking 하는 것
- mocking한다 = 실제로 함수를 동작시키는 것이 아니라 함수가 마치 동작한 것처럼 이 프로젝트를 속이는 것
- "main.get_todos"
- main.py 안에 있는 get_todos() 함수를 mocking 하게끔 설정한 것
- main.get_todos()는 원래 repository에 있음, 그런데 main.py에서 import 한 것
-
- return_value=[ ToDo(id=1, contents=”FastAPI Section 0”, is_done=True), ToDo(id=2, contents=”FastAPI Section 2”, is_done=False) ]
- return_value로 반환값 지정 가능
- main.py에 보면 get_todos_handler()의 반환값으로 Todo라는 값을 List로 반환해주도록 설정된 것 확인 (i.e., List[ToDo])
- 그러므로 여기에도 List로 감싸진 ToDo()를 반환하도록 설정
- 여기서는 get_todos() 함수에서 실제 DB를 조회하는 것이 아니라 우리가 여기에 설정한 mocking된 데이터 (i.e., [ ToDo(id=1, contents=”FastAPI Section 0”, is_done=True), ToDo(id=2, contents=”FastAPI Section 1”, is_done=False) ])를 반환하게 됨
- 마찬가지로 해당 데이터를 검증하는 부분도 수정 필요
- 위의 mocking.patch에서 return_value로 설정된 값들이 그 아래에 작성된 테스트 코드의 반환값에 해당하는 부분과 일치하도록 수정 필요
- # order ASC 에 해당하는 검증:
- assert response.json() == { “todos”: [ {“id”: 1, “contents”: “FastAPI Section 0”, “is_done”: True}, {“id”: 2, “contents”: “FastAPI Section 2”, “is_done”: False}, ] }
- # order DESC 에 해당하는 검증:
- assert response.json() == { “todos”: [ {“id”: 2, “contents”: “FastAPI Section 2”, “is_done”: False}, {“id”: 1, “contents”: “FastAPI Section 0”, “is_done”: True},] }
- from src.database.orm import ToDo
- $ pytest 실행
- 참조를 잘 못해서 에러가 날 경우 pytest를 실행할 경로를 같이 주면 문제 해결!
$ pytest
$ pytest src/tests
# 에러 1: pytest를 실행하는 경로가 잘못되었을 경우
$ pytest
============================= test session starts ============================================================================================
platform win32 -- Python 3.12.1, pytest-8.3.2, pluggy-1.5.0
rootdir: C:\Users\관리자\Desktop\projects\todos
plugins: anyio-4.3.0, mock-3.14.0
collected 136 items / 16 errors
============================== ERRORS ===================================================================================================
________________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_contextvars.py ___________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_contextvars.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
________________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_cpp.py _______________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_cpp.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_______________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_extension_interface.py _______________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_extension_interface.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_____________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_gc.py ________________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_gc.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_______________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_generator.py ____________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_generator.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
______________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_generator_nested.py _________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_generator_nested.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_____________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_greenlet.py _____________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_greenlet.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_____________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_greenlet_trash.py __________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_greenlet_trash.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
______________________________________________________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_leaks.py ______________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_leaks.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
____________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_stack_saved.py ___________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_stack_saved.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
___________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_throw.py ______________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_throw.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_______________________ ERROR collecting Lib/site-packages/greenlet/tests/test_tracing.py _____________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_tracing.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_version.py _____________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_version.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_____________________________ ERROR collecting Lib/site-packages/greenlet/tests/test_weakref.py _____________________________________________________________________
ImportError while importing test module 'C:\Users\관리자\Desktop\projects\todos\Lib\site-packages\greenlet\tests\test_weakref.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
Lib\site-packages\greenlet\tests\__init__.py:19: in <module>
import psutil
E ModuleNotFoundError: No module named 'psutil'
_____________________________ ERROR collecting Lib/site-packages/numpy __________________________________________________________________________________
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1387: in _gcd_import
???
<frozen importlib._bootstrap>:1360: in _find_and_load
???
<frozen importlib._bootstrap>:1331: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:935: in _load_unlocked
???
Lib\site-packages\_pytest\assertion\rewrite.py:174: in exec_module
exec(co, module.__dict__)
Lib\site-packages\numpy\conftest.py:7: in <module>
import hypothesis
E ModuleNotFoundError: No module named 'hypothesis'
_________________________________________________________________________________ ERROR collecting Lib/site-packages/pandas _________________________________________________________________________________
..\..\..\AppData\Local\Programs\Python\Python312\Lib\importlib\__init__.py:90: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1387: in _gcd_import
???
<frozen importlib._bootstrap>:1360: in _find_and_load
???
<frozen importlib._bootstrap>:1331: in _find_and_load_unlocked
???
<frozen importlib._bootstrap>:935: in _load_unlocked
???
Lib\site-packages\_pytest\assertion\rewrite.py:174: in exec_module
exec(co, module.__dict__)
Lib\site-packages\pandas\conftest.py:42: in <module>
import hypothesis
E ModuleNotFoundError: No module named 'hypothesis'
===================================== short test summary info ==========================================================================================
ERROR Lib/site-packages/greenlet/tests/test_contextvars.py
ERROR Lib/site-packages/greenlet/tests/test_cpp.py
ERROR Lib/site-packages/greenlet/tests/test_extension_interface.py
ERROR Lib/site-packages/greenlet/tests/test_gc.py
ERROR Lib/site-packages/greenlet/tests/test_generator.py
ERROR Lib/site-packages/greenlet/tests/test_generator_nested.py
ERROR Lib/site-packages/greenlet/tests/test_greenlet.py
ERROR Lib/site-packages/greenlet/tests/test_greenlet_trash.py
ERROR Lib/site-packages/greenlet/tests/test_leaks.py
ERROR Lib/site-packages/greenlet/tests/test_stack_saved.py
ERROR Lib/site-packages/greenlet/tests/test_throw.py
ERROR Lib/site-packages/greenlet/tests/test_tracing.py
ERROR Lib/site-packages/greenlet/tests/test_version.py
ERROR Lib/site-packages/greenlet/tests/test_weakref.py
ERROR Lib/site-packages/numpy - ModuleNotFoundError: No module named 'hypothesis'
ERROR Lib/site-packages/pandas - ModuleNotFoundError: No module named 'hypothesis'
!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 16 errors during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
======================================= 16 errors in 3.21s =============================================================================================
- module import error가 hypothesis 를 설치해야하는 줄 알고 hypothesis 패키지 설치함
- 그리고 후에 $ pytest todos/src 이렇게 경로를 줘서 pytest를 실행했었는데도 에러남
# 에러 2: 사용하는 함수/클래스들이 잘 참조 안 돼서 발생하는 에러
$ pytest src/tests
================================= test session starts ============================================================================================
platform win32 -- Python 3.12.1, pytest-8.3.2, pluggy-1.5.0
rootdir: C:\Users\관리자\Desktop\projects\todos
plugins: anyio-4.3.0, hypothesis-6.108.5, mock-3.14.0
collected 2 items
src\tests\test_main.py .F [100%]
======================================== FAILURES ==================================================================================================
_____________________________________ test_get_todos _______________________________________________________________________________________________
mocker = <pytest_mock.plugin.MockerFixture object at 0x000002A3664D60F0>
def test_get_todos(mocker): # 추가
# order = ASC
mocker.patch("main.get_todos", return_value=[ # 추가 - main.get_todos를 mocking하도록 설정
ToDo(id=1, contents="FastAPI Section 0", is_done=True), # 추가
ToDoSchema(id=2, contents="FastAPI Section 2", is_done=False), # 추가
]) # 추가
response = client.get("/todos")
assert response.status_code == 200
> assert response.json() == {
"todos": [
{"id": 1, "contents": "FastAPI Section 0", "is_done": True},
{"id": 2, "contents": "FastAPI Section 2", "is_done": False}, # True -> False 수정 (mocker의 내용과 동일하게끔)
# {"id": 3, "contents": "FastAPI Section 3", "is_done": True}, # 제거
]
}
E AssertionError: assert {'todos': [{'...done': True}]} == {'todos': [{'...one': False}]}
E
E Differing items:
E {'todos': [{'contents': 'FastAPI Section 0', 'id': 1, 'is_done': True}, {'contents': 'FastAPI Section 2', 'id': 2, '
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
src\tests\test_main.py:23: AssertionError
------------------------------------- Captured stdout call --------------------------------------------------------------------------------------------
2024-08-02 19:29:51,170 INFO sqlalchemy.engine.Engine SELECT DATABASE()
2024-08-02 19:29:51,171 INFO sqlalchemy.engine.Engine [raw sql] {}
2024-08-02 19:29:51,175 INFO sqlalchemy.engine.Engine SELECT @@sql_mode
2024-08-02 19:29:51,175 INFO sqlalchemy.engine.Engine [raw sql] {}
2024-08-02 19:29:51,178 INFO sqlalchemy.engine.Engine SELECT @@lower_case_table_names
2024-08-02 19:29:51,178 INFO sqlalchemy.engine.Engine [raw sql] {}
2024-08-02 19:29:51,183 INFO sqlalchemy.engine.Engine BEGIN (implicit)
2024-08-02 19:29:51,185 INFO sqlalchemy.engine.Engine SELECT todo.id, todo.contents, todo.is_done
FROM todo
2024-08-02 19:29:51,185 INFO sqlalchemy.engine.Engine [generated in 0.00024s] {}
2024-08-02 19:29:51,198 INFO sqlalchemy.engine.Engine ROLLBACK
-------------------------------------- Captured log call ---------------------------------------------------------------------------------------------
INFO sqlalchemy.engine.Engine:base.py:1846 SELECT DATABASE()
INFO sqlalchemy.engine.Engine:base.py:1846 [raw sql] {}
INFO sqlalchemy.engine.Engine:base.py:1846 SELECT @@sql_mode
INFO sqlalchemy.engine.Engine:base.py:1846 [raw sql] {}
INFO sqlalchemy.engine.Engine:base.py:1846 SELECT @@lower_case_table_names
INFO sqlalchemy.engine.Engine:base.py:1846 [raw sql] {}
INFO sqlalchemy.engine.Engine:base.py:2699 BEGIN (implicit)
INFO sqlalchemy.engine.Engine:base.py:1846 SELECT todo.id, todo.contents, todo.is_done
FROM todo
INFO sqlalchemy.engine.Engine:base.py:1846 [generated in 0.00024s] {}
INFO sqlalchemy.engine.Engine:base.py:2702 ROLLBACK
==================================== short test summary info ==========================================================================================
FAILED src/tests/test_main.py::test_get_todos - AssertionError: assert {'todos': [{'...done': True}]} == {'todos': [{'...one': False}]}
================================== 1 failed, 1 passed in 1.94s =======================================================================
- 후에 from ... import 문에서 내가 만든 파일들을 참조하는 것들만 밑줄이 나며 해당 모듈을 찾을 수 없다고 해서 (해당 단어 위에 커서를 올리면 그런 메세지가 뜸) 경로를 바꿔줬더니 pytest 검증이 잘 됨!
- 예) from schema.response import ToDoSchema → from src.schema.response import ToDoSchema
- mocking을 이용하여 테스트 코드가 동작된 것과 테스트가 통과된 것 확인 ㅠㅠ 오래걸렸다...
이때까지의 코드들:
- /c/Users/관리자/Desktop/projects/todos/src/main.py (수정 - import 부분 - "src." 추가)
- /c/Users/관리자/Desktop/projects/todos/src/database/orm.py (수정 - import 부분 - "src." 추가)
- /c/Users/관리자/Desktop/projects/todos/src/database/connection.py
- /c/Users/관리자/Desktop/projects/todos/src/database/repository.py
- /c/Users/관리자/Desktop/projects/todos/src/schema/request.py
- /c/Users/관리자/Desktop/projects/todos/src/schema/response.py
- /c/Users/관리자/Desktop/projects/todos/src/tests/test_main.py (수정 - 추가)
# /c/Users/관리자/Desktop/projects/todos/src/main.py 내용
from fastapi import FastAPI, Body, HTTPException, Depends
from sqlalchemy.orm import Session
from typing import List
from src.database.connection import get_db
from src.database.repository import get_todos, get_todo_by_todo_id, create_todo, update_todo, delete_todo
from src.database.orm import ToDo
from src.schema.response import ToDoSchema, ToDoListSchema
from src.schema.request import CreateToDoRequest
app = FastAPI()
# 첫 화면 API
@app.get("/")
def health_check_handler():
return {"ping": "pong"}
# GET Method 사용하여 전체 조회 API
@app.get("/todos", status_code=200)
def get_todos_handler(
order: str | None = None,
session: Session = Depends(get_db),
) -> ToDoListSchema:
todos: List[ToDo] = get_todos(session=session)
if order and order == "DESC":
# return todos[::-1]
return ToDoListSchema(
todos=[ToDoSchema.from_orm(todo) for todo in todos[::-1]]
)
# return todos
return ToDoListSchema(
todos=[ToDoSchema.from_orm(todo) for todo in todos]
)
# GET Method 사용하여 단일 조회 API
@app.get("/todos/{todo_id}", status_code=200)
def get_todo_handler(
todo_id: int,
session: Session=Depends(get_db),
) -> ToDoSchema:
todo:ToDo | None = get_todo_by_todo_id(session=session, todo_id = todo_id)
if todo:
return ToDoSchema.from_orm(todo)
raise HTTPException(status_code=404, detail="ToDo Not Found")
# POST Medthod 사용하여 todo 생성 API
@app.post("/todos", status_code=201)
def create_todo_handler(
request: CreateToDoRequest,
session: Session = Depends(get_db),
) -> ToDoSchema:
todo: ToDo = ToDo.create(request=request) # id=None
todo: ToDo = create_todo(session=session, todo=todo) # id=int
return ToDoSchema.from_orm(todo)
# PATCH Method 사용하여 is_done 값 수정 API
@app.patch("/todos/{todo_id}", status_code=200)
def update_todo_handler(
todo_id: int,
is_done: bool = Body(..., embed=True),
session: Session = Depends(get_db),
):
todo:ToDo | None = get_todo_by_todo_id(session=session, todo_id = todo_id)
if todo:
# update - is_done값이 True이면 todo.done() 실행, False이면 todo.undone() 실행
todo.done() if is_done else todo.undone()
todo: ToDo = update_todo(session=session, todo=todo)
return ToDoSchema.from_orm(todo)
raise HTTPException(status_code=404, detail="ToDo Not Found")
# DELETE Method 사용하여 todo 아이템 삭제 API
@app.delete("/todos/{todo_id}", status_code=204)
def delete_todo_handler(
todo_id: int,
session: Session = Depends(get_db),
):
todo:ToDo | None = get_todo_by_todo_id(session=session, todo_id = todo_id)
if not todo:
raise HTTPException(status_code=404, detail="ToDo Not Found")
delete_todo(session=session, todo_id=todo_id)
# /c/Users/관리자/Desktop/projects/todos/src/database/orm.py 내용
from sqlalchemy import Boolean, Column, Integer, String
from sqlalchemy.orm import declarative_base
from src.schema.request import CreateToDoRequest
Base = declarative_base()
# ToDo 클래스 모델링 한 것
class ToDo(Base):
__tablename__ = 'todo'
id = Column(Integer, primary_key=True, index=True)
contents = Column(String(256), nullable=False)
is_done = Column(Boolean, nullable=False)
def __repr__(self):
return f"ToDo(id={self.id}, contents={self.contents}, is_done={self.is_done})"
@classmethod
def create(cls, request: CreateToDoRequest) -> "ToDo":
return cls(
# id값은 DB에 의해 자동으로 결정 되서 별도로 지정안해줌
contents=request.contents,
is_done=request.is_done,
)
def done(self) -> "ToDo":
# ToDo의 is_done 값을 True로 변경 후 ToDo 반환
self.is_done = True
return self
def undone(self) -> "ToDo":
# ToDo의 is_done 값을 False로 변경 후 ToDo 반환
self.is_done = False
return self
# /c/Users/관리자/Desktop/projects/todos/src/database/connection.py 내용
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "mysql+pymysql://root:todos@127.0.0.1:3306/todos"
engine = create_engine(DATABASE_URL, echo=True)
SessionFactory = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
session = SessionFactory()
try:
yield session
finally:
session.close()
# /c/Users/관리자/Desktop/projects/todos/src/database/repository.py 내용
from sqlalchemy import select, delete
from sqlalchemy.orm import Session
from typing import List
from src.database.orm import ToDo
def get_todos(session: Session) -> List[ToDo]:
return list(session.scalars(select(ToDo)))
def get_todo_by_todo_id(session: Session, todo_id: int) -> ToDo | None:
return session.scalar(select(ToDo).where(ToDo.id == todo_id))
def create_todo(session: Session, todo: ToDo) -> ToDo:
session.add(instance=todo)
session.commit()
session.refresh(instance=todo)
return todo
def update_todo(session: Session, todo: ToDo) -> ToDo:
session.add(instance=todo)
session.commit()
session.refresh(instance=todo)
return todo
def delete_todo(session: Session, todo_id: ToDo) -> None:
session.execute(delete(ToDo).where(ToDo.id == todo_id))
session.commit()
# /c/Users/관리자/Desktop/projects/todos/src/schema/request.py 내용
from pydantic import BaseModel
class CreateToDoRequest(BaseModel):
contents: str
is_done: bool
# /c/Users/관리자/Desktop/projects/todos/src/schema/response.py 내용
from pydantic import BaseModel
from typing import List
class ToDoSchema(BaseModel):
id: int
contents: str
is_done: bool
class Config:
from_attributes = True
orm_mode = True # 이거 추가 해야지 ConfigError 안남
class ToDoListSchema(BaseModel):
todos: List[ToDoSchema]
# /c/Users/관리자/Desktop/projects/todos/src/tests/test_main.py 내용
from fastapi.testclient import TestClient
from src.schema.response import ToDoSchema
from src.main import app
from src.database.orm import ToDo # 추가
client = TestClient(app=app)
def test_health_check():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"ping": "pong"}
def test_get_todos(mocker): # 추가
# order = ASC
mocker.patch("src.main.get_todos", return_value=[ # 추가 - main.get_todos를 mocking하도록 설정
ToDo(id=1, contents="FastAPI Section 0", is_done=True), # 추가
ToDoSchema(id=2, contents="FastAPI Section 2", is_done=False), # 추가
]) # 추가
response = client.get("/todos")
assert response.status_code == 200
assert response.json() == {
"todos": [
{"id": 1, "contents": "FastAPI Section 0", "is_done": True},
{"id": 2, "contents": "FastAPI Section 2", "is_done": False}, # True -> False 수정 (mocker의 내용과 동일하게끔)
# {"id": 3, "contents": "FastAPI Section 3", "is_done": True}, # 제거
]
}
# order = DESC
response = client.get("/todos?order=DESC")
assert response.status_code == 200
assert response.json() == {
"todos": [
# {"id":3, "contents": "FastAPI Section 3", "is_done": True}, # 제거
{"id":2, "contents": "FastAPI Section 2", "is_done": False}, # True -> False 수정 (mocker의 내용과 동일하게끔)
{"id":1, "contents": "FastAPI Section 0", "is_done": True},
]
}
'Web 개발 > FAST API (인프런 강의 내용)' 카테고리의 다른 글
3 실습5 테스트 코드 - GET 단일조회 API (0) | 2024.08.05 |
---|---|
3 실습4 테스트 코드 - PyTest Fixture (0) | 2024.08.03 |
3 실습2 테스트 코드 - GET 전체조회 API (0) | 2024.08.01 |
3 실습1 PyTest 세팅 (0) | 2024.07.22 |
3 테스트 코드 PyTest (0) | 2024.06.01 |