본문 바로가기

백엔드(FastAPI)

[백엔드 > FastAPI] 8장. 테스트

테스트는 애플리케이션 개발 주기에서 빼놓을 수 없는 부분임

  • 애플리케이션이 정상적으로 실행되도록 보장하고 프로덕션에 배포하기 전에 이상 징후를 감지할 수 있음
  • 테스트 자동화 방법 (기존에는 수동으로 테스트함)

 

8장에서 다룰 핵심 내용은 다음과 같음

  • pytest를 사용한 단위 테스트(Unit test)
  • 테스트 환경 구축
  • REST API 라우트 테스트 작성
  • 테스트 커버리지 

8.1 pytest를 사용한 단위 테스트

단위 테스트

  • 애플리케이션의 개별 컴포넌트를 테스트하는 절차
  • 개별 컴포넌트의 기능을 검증하기 위해 수행

파이썬 테스트 라이브러리 pytest 를 활용한 단위 테스트

  • 파이썬은 unittest라는 내장 테스트 라이브러리를 제공함
    • but ! pytest 가 더 간단한 구문을 사용할 수 있으므로 인기가 많음 !! 

 

pytest 라이브러리 설치

$ pip install pytest

 

애플리케이션 테스트 파일을 한 곳에서 관리하기 위해 test라는 폴더를 만들고 파일을 생성함

$ mkdir tests && cd tests
$ touch __init__.py

 

테스트 파일을 만들 때는 파일명 앞에 'test_' 를 붙여야함

  • 해당 파일이 테스트 파일이라는 것을 pytest 라이브러리가 인식해서 실행함 

 

예시로, tests 폴더 아래 신규 테스트 파일을 하나 만들어본다

  • 이 테스트 파일은 사친연산이 맞는지 확인함
$ touch test_arithmetic_operations.py

 

테스트 대상 함수를 만듬

"""
	arithmetic_operations.py
"""

def add(a: int, b: int) -> int:
    return a + b

def subtract(a: int, b: int) -> int:
    return b - a

def multiply(a: int, b: int) -> int:
    return a * b

def divide(a: int, b: int) -> int:
    return b // a

 

테스트 함수를 만들어야함 

  • 테스트 함수는 계산 결과가 맞는지 검증하는 역할을 함
    • assert 키워드는 식의 왼쪽에 있는 값이 오른쪽에 있는 처리 결과와 일치하는지 검증할 때 사용됨
"""
	test_arithmetic_operations.py
"""
from arithmetic_operations import add,subtract,multiply,divide

def test_add() -> None:
    assert add(1, 1) == 2

def test_substract() -> None:
    assert subtract(2, 5) == 3

def test_multiply() -> None:
    assert multiply(10, 10) == 100

def test_divide() -> None:
    assert divide(25, 100) == 4

 

  1. 일반적으로 테스트 파일이 아닌 별도의 파일에 테스트 대상 함수를 정의함
  2. 이 파일(테스트 대상 함수)을 임포트하여 테스트를 수행함
$ pytest tests/test_arithmetic_operations.py

 

결과 화면

 

테스트 실패 예시

def test_add() -> None:
    assert add(1, 1) == 11

 

테스트 실패 결과

 

결과값이 2가 아니라 11로 되어있기 때문에 assert 문에서 테스트가 실패함

  • 실패 내용은 AssertionError에 요약되어 표시됨
    • 2 == 11 이 아니기 때문에 실패했다고 알려줌

 

pytest가 어떻게 실행되는지 간단히 살펴봄 

다음으로 pytest 의 픽서처(fixture)를 알아보도록 하자 

 

8.1 -(1) 픽스처를 사용한 반복 제거

픽스처 (fixture)

  • 재사용할 수 있는 함수
  • 테스트 함수에 필요한 데이터를 반환하기 위해 정의됨
  • pytest.fixture 데코레이터를 사용해 픽서처를 정의함
  • 용도 : API 라우트 테스트 시 애플리케이션 인스턴스를 반환하는 경우 등에 사용됨

테스트 함수가 사용하는 애플리케이션 클라이언트를 픽스처로 정의할 수 있기 때문에 테스트할 때마다 애플리케이션 인스턴스를
다시 정의하지 않아도 된다 ... ? ? 

  • 이 부분은 <8.3 REST API 라우트 테스트 작성> 에서 다룬다

 

픽스처 정의 방법

  • 픽스처를 어떻게 정의할까 ?
import pytest
from models.events import EventUpdate

# 픽스처 정의
@pytest.fixture
def event() -> EventUpdate:
    return EventUpdate(
        title="FastAPI Book Launch",
        image="https://packt.com/fastapi.png",
        description="We will be discussing the contents of the FastAPI book in\
                      this event. Ensure to come with your own copy to win gifts!",
        tags=["python", "fastapi", "book", "launch"],
        location="Google Meet"
    )

def test_event_name(event: EventUpdate) -> None:
    assert event.title == "FastAPI Book Launch"

< 코드 설명 >

  • EventUpdate pydantic 모델의 인스턴스를 반환하는 픽서처를 정의함
  • 이 픽시처는 test_event_name() 함수의 인수로 사용되며 이벤트 속성에 접근할 수 있음 

 

  • 픽스처 데코레이터는 인수를 선택적으로 받을 수 있음
    • (예) scope 인수는 픽스처 함수의 유효 범위를 지정할 때 사용됨 
    • 여기서는 두가지 scope 를 사용함 
      • session : 테스트 전체 세션 동안 해당 함수가 유효하다.
      • module : 테스트 파일이 실행된 후 특정 함수에서만 유효하다.
# 픽스처 정의
@pytest.fixture
def event(Scope parameter : (1) session or (2) module) -> EventUpdate:
    return EventUpdate(
        title="FastAPI Book Launch",
        image="https://packt.com/fastapi.png",
        description="We will be discussing the contents of the FastAPI book in\
                      this event. Ensure to come with your own copy to win gifts!",
        tags=["python", "fastapi", "book", "launch"],
        location="Google Meet"
    )

 

8.2 테스트 환경 구축

CRUD 처리용 라우트와 사용자 인증을 테스트해보고자함

  • 비동기 API를 테스트하려면 httpx와 pytest-asyncio 라이브러리를 설치해야함
$ pip install httpx pytest-asyncio

 

설치가 완료됐으면, 

  • pytest.ini 라는 설정 파일을 만들어야함
  • 파일을 루트 폴더 (main.py가 있는 폴더)에 생성한 후 아래와 같이 코드를 추가함
"""
	pytest.ini 
"""

[pytest]
asyncio_mode = auto

 

pytest가 실행 될 때 이 파일의 내용을 불러옴

  • 위와 같이 설정을 하는 이유 : pytest가 모든 테스트를 비동기식으로 실행한다는 의미임

설정 파일 준비가 끝났으니,

  • tests 폴더 아래에 테스트 시작점이 될 conftest.py 파일을 만듬 
  • conftest.py 파일을 왜 만듬? 
    • 테스트 파일이 필요로 하는 애플리케이션의 인스턴스를 만듬

conftest.py 파일을 생성하고,

$ touch tests/conftest.py

 

conftest.py 파일에 의존 라이브러리를 임포트함 

import asyncio
import httpx
import pytest

from main import app
from database.connection import Settings
from models.events import Event
from models.users import User
  • asyncio, httpx, pytest 를 임포트함
    • asyncio 모듈
      • 활성 루프 세션을 만들어서 테스트가 단일 스레드로 실행되도록함
    • httpx 테스트
      • HTTP CRUD 처리를 실행하기 위한 비동기 클라이언트 역할을 함 
    • pytest 라이브러리
      • 픽스처 정의를 위해 사용됨
      • 애플리케이션 인스턴스(app) , Settings 클래스, 모델도 임포트

 

루프 세션 픽스처를 정의해보기 

@pytest.fixture(scope="session")
def event_loop():
	loop = asyncio.get_event_loop()
    yield loop
    loop.close()

 

Settings 클래스에서 새로운 데이터베이스 인스턴스를 만듬

"""
 database > connection.py > Settings 클래스
 
"""

	async def init_db():
        test_settings = Settings()
        test_settings.DATABASE_URL = "mongodb://localhost:27017/testdb"
        
        await test_settings.initialize_database()

< 코드 설명 >

  • DATABASE_URL과 <6장. 데이터베이스 연결> 에서 정의한 초기화 함수를 호출함 
  • testdb 라는 새로운 데이터베이스를 사용함 

 

마지막으로,

기본 클라이언트 픽스처를 정의함

  • 이 픽스처는 httpx를 통해 비동기로 실행되는 애플리케이션 인스턴스를 반환함
@pytest.fixture(scope="session")
async def default_client():
    await init_db()
    async with httpx.AsyncClient(app=app, base_url="http://app") as client:
    	yield client
        # 리소스 정리
        await Event.find_all().delete()
        await User.find_all().delete()

 

< 코드 설명 >

  • 데이터베이스를 초기화한 후에 애플리케이션을 AsyncClient로 호출함
    • AsyncClient 는 테스트 세션이 끝날 때까지 유지됨
    • 테스트 세션이 끝나면 이벤트(Event)와 사용자(User) 컬렉션의 데이터를 모두 삭제하여
      테스트를 실행할 때마다 데이터베이스가 비어있도록 함

-------- 지금까지 테스트 환경 구축 방법을 살펴봄 --------

 

8.3 REST API 라우트 테스트 작성

test_login.py 파일을 작성해서 인증 로직을 테스트 해보도록함

$ touch tests/test_login.py

 

1. 필요한 의존 라이브러리를 임포트함

import httpx
import pytest

 

8.3 - (1) 사용자 등록 라우트 테스트

 

첫번째 테스트 대상은 사용자 등록 라우트임 

  • pytest.mark.asyncio 데코레이터를 추가해서 비동기 테스트라는 것을 명시함
  • 아래와 같이 테스트 함수와 요청 페이로드를 정의함 
"""
    tests/test_login.py
    Method : test_sign_new_user
    Param : default_client: httpx.AsyncClient
"""

import httpx
import pytest

@pytest.mark.asyncio
async def test_sign_new_user(default_client: httpx.AsyncClient) -> None:
    payload = {
        "email": "testuser@packt.com",
        "password": "testpassword",
    }
    # 요청 헤더와 응답을 정의함
    headers = {
        "accept": "application/json",
        "Content-Type": "application/json"
    }
    test_response = {
        "message": "User created successfully."
    }
    # 요청에 대한 예상 응답을 정의함
    response = await default_client.post("/user/signup", json=payload,
                                         headers=headers)
    # 응답을 비교해서 요청이 성공했는지 확인하는 코드 작성
    assert response.status_code == 200
    assert response.json() == test_response

 

몽고DB 서버가 실행되고 있는 상태에서 별도의 터미널 창을 열어 테스트를 실행함 

$ pytest tests/test_login.py

 

  • 파이썬 버전에 따라 import 문이 인식되지 않아서 실행되지 않을 수 있음 !
  • 이 경우에는 명령어 앞에 python -m 을 붙여서 실행하면 해결됨 (이후 테스트 실행시에도 활용)
$ python -m pytest tests/test_login.py

 

사용자 등록 , 사용자 로그인 라우트 테스트가 성공한 화면 ( 그런데 1 error 인 loop 에러 발생 !! 이유는 알아보는중 .. ) 

 

  • 테스트 응답을 변경해서 테스트가 실패했는지 확인해보는 것도 좋음 ! 
  • 메일이 중복되면, 409 에러가 나오는 것을 확인함

 

 

 

 

 

'백엔드(FastAPI)' 카테고리의 다른 글

[백엔드 > FastAPI] 7장. 보안  (0) 2024.01.20
[백엔드 > FastAPI] 5장. 구조화  (0) 2024.01.06
[백엔드 > FastAPI] 2장. 라우팅  (0) 2023.12.23