FastAPI/테스트 코드

POST API

hyunai 2026. 7. 10. 17:10
# request.py
from pydantic import BaseModel

class CreateToDoRequest(BaseModel):
    contents: str
    is_done: bool


# main.py
@app.post("/todos", status_code=201)
# FastAPI가 알아서 reqeustBody를 CreateToDoRequest에 넣어서 처리를 해준다.
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.model_validate(todo)
    
    
# orm.py
    # 요청 받은 requestBody를 ORM 객체로 변환해주는 클래스 method
    @classmethod
    def create(cls, request : CreateToDoRequest) -> "ToDo":
        return cls(
            contents=request.contents,
            is_done=request.is_done,
        )
        

# test_main.py
def test_create_todo(client, mocker):
    mocker.patch(
        "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 response.status_code == 201
    assert response.json() == {"id": 1, "contents": "todo", "is_done": True}
  • main.py의 create_todo를 mocking한다.
  • post 요청으로 create를 해주기 때문에 requestbody가 필요하다.
    • request.py 파일에 있는 객체 멤버변수인 contents, is_done 값을 body로 전달해준다.
  • 이러면 pytest를 했을 때 pass는 나지만 뭔가 이상한 점이 발견된다.

이상한 점?

    body = {
        "contents" : "test",
        "is_done" : False,
    }

 

    mocker.patch(
        "main.create_todo",
        return_value=ToDo(id=1, contents="todo", is_done=True),
    )    
...
    assert response.json() == {"id": 1, "contents": "todo", "is_done": True}
  • 지금 requestbody는 contents는 test, is_done값은 False값을 넘기고 있다.
    • 하지만 마지막 response 데이터에서는 contents 값이 todo이고 is_done값이 True이다.
      • 다시 말해서 이 response가 실제로 응답해서 그걸 검증하는 부분이 mocking 하는 부분과는 일치하지만 요청한 requestbody 부분과는 지금 일치하지 않는다.

요청 흐름 시각화

client.post("/todos", json=body)
body = {"contents": "test", "is_done": False}
        ↓
create_todo_handler 실행
        ↓
ToDo.create(request=request)   ← body로 ToDo 객체 생성
todo = ToDo(contents="test", is_done=False)  id=None
        ↓
create_todo(session, todo) 호출 시도
        ↓
mocker가 가로챔
"DB에 저장하는 거 내가 대신할게"
return_value=ToDo(id=1, contents="todo", is_done=True)
        ↓
{"id": 1, "contents": "todo", "is_done": True} 반환

mocker가 대체하는 시점이 핵심

어디서 mocker가 개입하냐면

def create_todo_handler(request, session):
    todo = ToDo.create(request=request)
    # todo.contents = "test"    ← body 값
    # todo.is_done = False      ← body 값
    # todo.id = None            ← 아직 DB 저장 전

    todo = create_todo(session=session, todo=todo)
    #      ↑
    #   여기서 mocker가 가로챔!
    #   body 값은 무시되고
    #   return_value 값이 반환됨
    #   todo.id = 1
    #   todo.contents = "todo"
    #   todo.is_done = True

    return ToDoSchema.model_validate(todo)
    # mocker가 준 값으로 응답

body랑 mocker 데이터가 달라도 pass인 이유

각 데이터의 역할

데이터  역할 응답에 영향?
body = {"contents": "test", "is_done": False} create_todo 호출 전까지만 사용 X
mocker return_value = ToDo(id=1, contents="todo", is_done=True) create_todo 결과로 반환됨 O

body는 create_todo() 를 호출하는 데 쓰이는 값이고, mocker는 create_todo() 의 반환값을 대체하기 때문


이 테스트가 실제로 검증하는 것

이 테스트의 진짜 목적

# 이 테스트가 검증하는 것:
# 1. POST /todos 요청이 201 상태코드를 반환하는가?
assert response.status_code == 201

# 2. create_todo()의 반환값을 ToDoSchema로 잘 변환해서 응답하는가?
assert response.json() == {"id": 1, "contents": "todo", "is_done": True}

# 이 테스트가 검증하지 않는 것:
# - body 값이 실제 DB에 잘 저장되는가? (mocker로 대체했으니까)
# - contents="test", is_done=False 가 제대로 전달되는가?

💡 핵심 요약:
body 값은 create_todo() 를 호출할 때 쓰이는 입력값
mocker는 create_todo() 의 출력값을 대체
이 테스트는 "DB 저장이 잘 되는가" 가 아니라
"create_todo 결과를 받아서 응답을 잘 만드는가" 를 검증 것


mocker의 spy 사용해보기

def test_create_todo(client, mocker):
	# ToDo라는 인자의 create라는 함수를 spy할 것이다. 라는 의미로 변수로 할당
    create_spy = mocker.spy(ToDo, "create")
    mocker.patch(
        "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}
  • spy_return을 할 때 ToDo와 연결이 되어 있다.
    • 그래서 해당 ToDo 코드가 호출이 되면 return 값이 원래는 확인을 못했지만 spy를 사용하여 spy_return 안에 복사가 된다.
    • 따라서 지금 이 spy 코드들은 ToDo가 잘 생성됐는지 검증해주는 코드

'FastAPI > 테스트 코드' 카테고리의 다른 글

GET 단일 조회 API  (0) 2026.07.07
PyTest Fixture  (0) 2026.07.07
PyTest Mocking  (0) 2026.07.07
GET 전체 조회 API  (0) 2026.07.06
PyTest란?  (0) 2026.07.01