앞 단계 참조 링크:
- 상태 코드: 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
- 3 실습3 테스트 코드 PyTest Mocking: 2024.08.02 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습3 테스트 코드 - PyTest Mocking
- 3 실습4 테스트 코드 PyTest Fixture: 2024.08.03 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습4 테스트 코드 - PyTest Fixture
- 3 실습5 테스트 코드 GET 단일조회 API: 2024.08.05 - [Web 개발/FAST API (인프런 강의 내용)] - 3 실습5 테스트 코드 - GET 단조회 API
00 개요
- 목적: ToDo를 생성하는 create_todo_handler()로 정의된 POST API에 테스트 코드 추가하는 실습 진행
01 POST API에 테스트 코드 추가하기
1. main.py 확인
- 테스트 코드 적용할 API 확인하기:
# /c/Users/관리자/Desktop/projects/todos/src/main.py 내용
...
# 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)
...
- mocking 적용할 부분:
- todo: ToDo = ToDo.create(request=request) → orm.py의 ToDo 클래스의 create() 메소드
- todo: ToDo = create_todo(session=session, todo=todo) → repository.py의 create_todo() 함수
- 정보:
- post 요청 사용
- 경로: "/todos"
- 함수명: 'create_todo_handler'
- 요청 값 (request): request.py의 CreateToDoRequest()
- 정상적인 반환값: ToDoSchema.from_orm(todo)
2. test_main.py에 쓰기
- spy 기능 추가
- spy: 특정 객체를 tracking해서 (i.e., 따라가서) 그 객체의 return 값 또는 어떤 연산들을 했는지를 그 객체 안에 저장
- def test_create_todo()를 test_main.py의 끝부분에 다음을 추가:
# /c/Users/관리자/Desktop/projects/todos/src/tests/test_main.py 내용
...
def test_create_todo(client, mocker): # 추가
create_spy = mocker.spy(ToDo, "create") # spy 객체 생성
# create_todo() 검증
mocker.patch(
"src.main.create_todo",
return_value=ToDo(id=1, contents="todo", is_done=True),
)
# ToDo.create() 검증
body = { # ToDo.create()가 request body를 받기 때문에 request body 정의 필수
"contents": "test",
"is_done": False,
}
response = client.post("/todos", json=body)
assert create_spy.spy_return.id is None
assert create_spy.spy_return.contents == "test"
assert create_spy.spy_return.is_done is False
assert response.status_code == 201
assert response.json() == {"id":1, "contents":"todo", "is_done":True}
- 설명:
- def test_create_todo(client, mocker):
- test_create_todo() 함수 정의 시 client fixture와 mocker fixture 사용
- create_spy = mocker.spy(ToDo, "create")
- mocker.spy(spy하는대상_클래스객체, "spy하는 대상_클래스_메소드")로 spy 객체 생성
- 즉, spy하는 대상 = orm.py의 ToDo.create()
- mocker.patch("src.main.create_todo", return_value=ToDo(id=1, contents="todo", is_done=True),)
- mocker.patch("mocking적용함수", return_value="반환값") 함수를 통해 mocking을 적용할 함수와 반환값 설정
- 반환값은 src.main.create_todo()가 ToDo를 하나 생성해서 return된 값
- body = {"contents": "test", "is_done": False,}
- create 요청이기 때문에 request body 요청 필수
- main.py에 보면 request로 request.CreateToDoRequest를 전달해줘야 되는 것 확인
- requests.py에 보면 clas CreateToDoRequest():는 contents:str과 is_done: bool를 요청하고 있는 것 확인
- 그러므로 request body를 정의할 이 body에 contents와 is_done를 포함
- response = client.post("/todos", json=body)
- post 메소드 적용
- "/todos" path로 post 요청 보내도록 설정
- 앞서 생성한 body를 json 키에 전달 시 TestClient (= client fixture →우 리가 conftest.py에서 정의함) request body로 처리하게 됨
- assert create_spy.spy_return.id is None
- assert create_spy.spy_return.contents == "test"
- assert create_spy.spy_return.is_done is False
- 우리가 생성한 create_spy 객체에는 우리가 지정한 함수의 반환값을 담는 spy_return 속성이 있음
- 우리가 spy한 부분: main.py에서 todo: ToDo = ToDo.create(request=request)
- main.py의 create_todo_handler() 함수에서 ToDo.create() 코드가 호출이 되면 해당 반환값이 create_spy 객체에 복사가 됨
- spy_return의 속성값으로 조회 가능
- 위에 정의한 request body로 POST시 "create" 함수의 반환값은 다음과 같을 것:
- {"id": None, "contents": "test", "is_done": False,}
- id가 None인 이유: db에 저장이 안 되었기 때문
- contents 및 is_done값은 위에 body (i.e., request body)에서 설정/정의한대로 작성해
- 이 반환값을 확인하기 위해 위해 같이 작성
- {"id": None, "contents": "test", "is_done": False,}
- assert response.status_code == 201
- 201 상태 코드 반환 검증 확인
- assert response.json() == {"'id":1, "contents":"todo", "is_done":True}
- response 값에 사용될 우리가 만든 ToDo
- def test_create_todo(client, mocker):
3. 검증 2가지 방법
1) 모든 테스트 한 번에 실행
- src 디렉토리에서 $ pytest 명령어 실행
- 실행 전 uvicorn이 띄워져있는지, docker가 실행중인지 확인해야 됨
$ pytest
# 또는 다른 경로에 있을 경우 tests 경로를 지정해서 실행
# 예시 - todos/src 경로에 있을 경우
$ pytest tests
- 정상적으로 통과된 것을 확인!
- 메세지: ================= 4 passed in 0.26s ===================
2) 1개의 테스트 실행
- src 디렉토리에서 $pytest 이후에 원하는 함수 경로 지정하여 실행
$ pytest 상대경로::함수명
$ pytest tests/test_main.py::test_create_todo
- main.py의 51, 52, 53번 줄들도 검증 확인!
mocking 사용 시 주의사항:
- 검증을 누락하는 함수 있는지 잘 확인 필수!
- todo: ToDo = ToDo.create(request=request)와
- todo: ToDo = create_todo(session=s
- 각각 따로 검증해야 됐음!
FYI:
- spy 기능 대신 equal magic method를 override하는 방식으로도 해결 가능
- 그러나 현재 단계에서는 equal magic method를 잘 이해하기에는 어렵다고 판단되기에 일단 spy 기능 사용하여 테스트 코드 진행!
이때까지의 코드들: 8개
- /c/Users/관리자/Desktop/projects/todos/src/main.pㅛ
- /c/Users/관리자/Desktop/projects/todos/src/database/orm.py
- /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/tests/conftest.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/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 src.schema.response import ToDoSchema
from src.database.orm import ToDo
def test_health_check(client): # client 는 내가 conftest.py에서 정의한 fixture
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"ping": "pong"}
def test_get_todos(client, mocker):
# order = ASC
mocker.patch("src.main.get_todos", return_value=[
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},
]
}
# order = DESC
response = client.get("/todos?order=DESC")
assert response.status_code == 200
assert response.json() == {
"todos": [
{"id":2, "contents": "FastAPI Section 2", "is_done": False},
{"id":1, "contents": "FastAPI Section 0", "is_done": True},
]
}
def test_get_todo(client, mocker):
# 상태코드 200
mocker.patch(
"src.main.get_todo_by_todo_id", # mocking 적용할 함수 및 반환값 설정
return_value = ToDo(id=1, contents="todo", is_done=True),
)
response = client.get("/todos/1") # path에 하위 서브 path 적어주기
assert response.status_code == 200
assert response.json() == {"id":1, "contents": "todo", "is_done": True }
# 상태코드 404
mocker.patch(
"src.main.get_todo_by_todo_id", # mocking 적용할 함수 및 반환값 설정
return_value = None,
)
response = client.get("/todos/1")
assert response.status_code == 404
assert response.json() == {"detail":"ToDo Not Found"}
def test_create_todo(client, mocker):
# 상태코드 201
create_spy = mocker.spy(ToDo, "create")
mocker.patch(
"src.main.create_todo",
return_value=ToDo(id=1, contents="todo", is_done=True),
)
body = {
"contents": "test",
"is_done": False,
}
response = client.post("/todos", json=body)
assert create_spy.spy_return.id is None
assert create_spy.spy_return.contents == "test"
assert create_spy.spy_return.is_done is False
assert response.status_code == 201
assert response.json() == {"id":1, "contents":"todo", "is_done":True}
# /c/Users/관리자/Desktop/projects/todos/src/tests/conftest.py 내용
import pytest
from fastapi.testclient import TestClient
from src.main import app
@pytest.fixture
def client():
return TestClient(app=app)
'Web 개발 > FAST API (인프런 강의 내용)' 카테고리의 다른 글
3 실습8 테스트 코드 - DELETE API (0) | 2024.08.07 |
---|---|
3 실습7 테스트 코드 - PATCH API (0) | 2024.08.05 |
3 실습5 테스트 코드 - GET 단일조회 API (0) | 2024.08.05 |
3 실습4 테스트 코드 - PyTest Fixture (0) | 2024.08.03 |
3 실습3 테스트 코드 - PyTest Mocking (0) | 2024.08.02 |