[FastAPI] 12. Pytest / UnitTest를 이용한 테스트 코드 작성

반응형

이번 포스트에서는 Pytest와 Unit Test를 이용하여 FastAPI에서 작성한 API를 테스트할 수 있는 방법에 대해 알아보겠습니다.

 

 

Pytest

Python 언어에서 지원하는 테스트 프레임워크에는 Pytest가 있습니다. Pytest는 Python에서 간단한 코드에 대해 심플하고 간결한 테스트 코드를 작성할 수 있도록 도와주며 다양한 플러그인들을 활용하여 규모가 크고 복잡한 애플리케이션에 대해서도 테스트를 쉽게 할 수 있도록 도와주는 프레임워크입니다.

 

주요 특징으로 아래와 같습니다.

 

  • 테스트 코드 실행에 실패할 경우 자세한 정보 표시 지원
  • 테스트 모듈 및 기능을 자동으로 검색해주는 기능 지원
  • Session과 같은 수명이 긴 리소스를 매개변수화 해주고, 이를 관리하기 위한 매커니즘 제공
  • unittest, nose test와 같은 다른 테스트 제품군 호환
  • Python 3.6, PyPy 3 지원

 

특히 Pytest에서 지원하는 Fixture 기능은 테스트 코드 작성을 더욱 용이하게 해줍니다.

 

 

 

UnitTest

UnitTest는 Python에서 사용할 수 있는 단위 테스트 프레임워크로 Java의 단위 테스트 프레임워크인 JUnit에서 영감받은 테스트 프레임워크입니다. 주요 기능으로 테스트 자동화, 테스트 생명 주기, 테스트 통계를 지원해줍니다.

 

실제로 FastAPI에서 Layered Architecture를 사용하는 경우 이러한 아키텍처를 잘 맞춰진 상태로 코드가 실행되고 있는지를 테스트 해보고 싶을 때가 생기는데, 이 때 UnitTest를 사용하면 매우 유용하게 활용할 수 있습니다. 

 

특히 UnitTest는 Pytest와 달리 객체 지향(Object-oriented) 방식을 지원하여 JUnit과 마찬가지로 테스트 실행을 조절할 수 있고, 이를 마음대로 주무를 수도 있습니다. UnitTest에서 지원하는 Test Runner는 PyQt와 같은 GUI 애플리케이션 테스트에 아주 좋습니다.

 

 

 

 

How to use

본론으로 넘어와서 두 테스트 프레임워크로 어떻게 테스트 코드를 작성할 수 있는지 알아보겠습니다. 시작하기 전에, 자신이 테스트 코드를 클래스로 작성하고 싶다면 UnitTest를, 단순 함수로 작성하고 싶다면 Pytest를 이용하는 걸 추천합니다.

 

먼저 UnitTest를 이용하여 Class-like 테스트 코드를 작성해보겠습니다.

 

Class-like의 테스트 코드를 작성하기 위해 unittest 패키지의 TestCase를 상속합니다. TestCase 클래스에서 제공하는 setUp 함수는 테스트 코드를 시작할 때 처음으로 실행되는 TestCase 클래스의 콜백 함수입니다.

 

이러한 콜백 함수는 상속에도 똑같이 적용되며 각 테스트 케이스에 초기 설정이 필요한 경우에 유용하게 사용할 수 있습니다.

UnitTest는 assert를 위한 assertArrayEquals 등의 클래스 메서드를 지원합니다.

 

다음은 Pytest를 이용한 Functional 테스트 코드를 작성해보겠습니다.

 

OOP에 익숙하지 않으신 분들이라면 오히려 일반 함수로 작성하는 것이 더 깔끔해 보일 수도 있습니다. 이렇게 작성된 테스트 코드는 아래의 명령어로 실행할 수 있습니다.

 

$ pytest -v

기본적으로 pytest는 접두사 test_*.py 파일을 자동으로 읽어 해당 코드만을 실행합니다. 만약 다른 파일도 같이 돌리고자 한다면 디렉터리를 지정하거나 모듈 이름을 명렁어 뒤에 넣어 실행하면 됩니다. 별도의 flag는 사용하지 않습니다.

 

 

 

 

FastAPI Test

FastAPI로 작성한 코드는 어떻게 테스트할 수 있을까요? 기본적으로 서버의 테스트는 integration Test (통합 테스트)를 통해 애플리케이션의 품질을 검증합니다. 

 

  • 테스트 환경으로 구성된 Application 실행 
  • HttpClient 라이브러리를 이용한 API 호출 테스트

 

통합 테스트는 위 두 가지 과정을 통해 테스트가 진행되며 애플리케이션에 포함된 모든 미들웨어가 준비되어 있어야 합니다. Java Spring의 경우에는 Local Database로 H2를 사용할 수도 있고, FastAPI의 경우에는 SQLite를 사용해볼 수도 있습니다.

 

하지만 이 포스트에서는 실제로 여러분들이 자주 사용하는 관계형 DB를 직접 사용해볼 것이며 그 중에서도 PostgreSQL을 사용해서 예제를 보여드릴 것입니다. 

 

 

테스트 모듈의 구조는 위와 같습니다. test_memo.py에 우리가 이전에 작성했던 Memo Application에 대한 API 테스트 코드를 작성할 것이며 conftest.py에는 우리가 사용할 test fixture를 작성할 것입니다.

 

우선 test/conftest.py에 우리가 사용할 fixture를 정의합니다.

 

FastAPI는 Starlette 프레임워크를 기반하고 있으므로 Starlette에서 제공하는 TestClient를 사용할 수 있습니다. 단, TestClient는 requests 디펜던시를 사용하므로 requests 디펜던시를 설치한 후 이용해야 합니다.

 

conftest.py에 등록된 fixture는 별도의 import 없이 바로 테스트 함수의 파라미터로 사용할 수 있습니다. 미리 정의된 fixture로 DB에 데이터를 영속시킨 후 API를 호출했을 때 제대로 조회가 되는지를 확인합니다.

 

그리고, POST 메서드에 대해서는 Request json을 미리 정의하고 api로 호출합니다. 마지막으로 미리 정의된 fixture로 db에서 데이터를 조회한 후 데이터가 맞는지를 확인하면 테스트 코드가 완성됩니다.

 

테스트가 잘 수행되고 있음을 볼 수 있습니다.

 

 

 

 

AsyncClient Test

방금 전까지의 테스트 코드는 동기 클라이언트 라이브러리인 requests를 사용하여 동기 테스트를 진행한 것입니다. 만약, 비동기 함수로 API 코드를 작성했다면 비동기에 대한 테스트가 필요합니다. 

 

이 때는 requests 디펜던시가 아닌 httpx 디펜던시를 이용하여 테스트 코드를 작성해 볼 수 있습니다.

여기서는 AsyncClient 외에 test_db_session 또한 asyncpg CP를 이용하여 async_session으로 테스트를 진행 해보도록 하겠습니다.

 

pytest에서 비동기 테스트 코드를 사용할 때는 pytest-asyncio 디펜던시를 별도로 설치하여야 합니다.

 

통합 테스트에서는 LifespanManager를 사용하여 FastAPI의 startup, shutdown 메서드를 동작시킬 수 있도록 해야 합니다. 그렇지 않으면 Connection Pool과 같은 작업들이 진행되지 않아 테스트 코드가 정상 동작하지 않습니다.

 

그리고 모든 테스트 코드가 asyncio의 이벤트 루프를 사용할 수 있도록 fixture에 asyncio의 이벤트 루프를 추가해줍니다. 파이썬의 이벤트 루프에 대해서 잘 모르시겠다면 파이썬 공식 문서 asyncio를 참고해보세요.

 

모든 테스트 함수에 pytest.mark.asyncio 데코레이터를 붙여준 뒤 async await 구분을 사용하도록 합니다. SQLAlchemy의 경우 DB에서 데이터 조회시 AsyncSession을 사용하는 경우 SQLAlchemy 2.x 문법을 사용합니다.

 

정상적으로 동작한다면 위와 같이 passed가 출력됩니다.

 

 

 

 

Layered Architrecture Test

Python의 UnitTest에서는 객체를 Mocking 할 수 있는 Mock 함수를 지원합니다. 우리는 이를 이용해서 만든 애플리케이션을 정해진 아키텍처에 맞춰 함수가 호출되고 있는지를 유닛 테스팅 할 수 있습니다.

 

API Endpoint에 대한 테스트를 진행하는 경우 FastAPI가 가지고 있는 DI의 override를 사용하여 해당 디펜던시를 Mocking해 테스트 해볼 수 있습니다.

 

그런 다음 assert_called_once_with 혹은 assert_called_with 함수를 이용하여 해당 Layer에서 하위 레이어가 잘 호출되고 있는지를 테스트 해볼 수 있습니다.

 

Dependency Injector를 사용한다면 해당 컨테이너에 정의된 디펜던시를 제시한 후 override 함수를 이용해서 해당 디펜던시를 모킹할 수 있습니다. 이 때 Context Manager를 사용합니다.

 

 

 

 

마치며...

여기까지 Pytest, UnitTest를 이용하여 FastAPI로 만든 애플리케이션을 테스트 해볼 수 있는 방법에 대해 알아봤습니다.

 

파이썬에서는 프로세스, 스레드 외에 이벤트 루프인 asyncio를 이용하여 비동기 매커니즘을 사용해 동시성 처리할 수 있다보니 테스트 코드에 대해서도 이러한 부분을 신경써줘야 하며 특히 의존성 주입이 어떤식으로 이뤄지고 어떻게 사용해야 하는지에 대한 충분한 이해와 숙지가 필요합니다.

 

조금 눈치 채신 분들도 계시겠지만 Layered Architrecture 내지 DDD를 사용한다면 유닛 테스트를 이용해 TDD를 해볼 수도 있겠다 라는 생각을 하셨을 것입니다. 맞습니다. 이들 디펜던시를 이용해서 충분히 Python 백엔드 애플리케이션 개발에도 다양한 개발 방법론을 이용해 볼 수 있습니다. 

 

여기에 behave 디펜던시를 사용한다면 Given When Then 패턴의 BDD를 해볼 수도 있는데요. 이들 컨텐츠에 대해서는 차후 다른 포스트에서 만나보도록 하겠습니다.

 

 

반응형