본문 바로가기

Development/Laboratrix

[Laboratrix] 5 - Backend API Unit Test

 

 

* 목차

 - Intro

 - Unit Test 간단 설명

 - Unit Test Code 작성

 - Outro

 

 


Intro

이전 글에서는 Fastapi를 활용해서 Backend API를 만들어 봤습니다.

 

[Laboratrix] 4 - FastAPI로 CRUD 동작 구현하기

Intro이전 시간에는 Docker Compose로 Backend 개발 환경을 구성했었습니다. [Laboratrix] 3 - Docker Compose로 backend 개발 환경 구성* 목차 - Intro - Install Docker Desktop - Install vscode - Container 접속 - Docker Container

tyoon9781.tistory.com

 

하지만 그것가지고는 그림을 완성할 수 없습니다. Unit Test를 진행해보도록 하겠습니다.

 

 


Unit Test 간단 설명

Unit Test는 처음에 할 때는 지겹기만 하고 왜 하는지 잘 모르는 경우가 많습니다. 실제로도 처음에는 아무런 문제가 없어보이는 코드에 굳이 Test를 만드는 것이 이해가 안되기도 하죠. 하지만 Code를 유지보수하다보면 정상적으로 동작해야 할 Code가 아무런 증상 없이 "틀린" 동작을 Error Raise 없이 하기 시작합니다. 이렇게 되면 문제가 되는 데이터가 쌓이게 되고 다시 복구하는데 어려움을 겪게 됩니다. 이러한 것을 방지하기 위한 것이 Unit Test라 생각하면 됩니다.

 

python에서 주로 사용하는 unit test tool은 2가지가 있습니다.

  • unitttest : python 기본 library입니다. Class를 기반으로 Test를 진행합니다.
  • pytest : python unit test를 위한 framework입니다. Function을 기반으로 Test를 진행합니다.

 

저는 여기서 unittest로 진행해보도록 하겠습니다.

 

fastapi로 unittest를 진행하기 위해서는 TestClient를 사용해야 하는데 이 모듈은 httpx를 설치해야 합니다. 

pip install httpx

 

 

설치가 완료되었으면 Unit Test Code를 작성해 보도록 합시다.

 

 

 


Unit Test Code 작성

unit test를 진행할 code를 작성해 보겠습니다. test.py 파일을 만들고 작성해 보겠습니다.

import unittest
from fastapi.testclient import TestClient
from fastapi import status
from sqlalchemy.orm import Session

from local_main import app
from app.db.models.item import ItemCreate
from app.db.connection import create_tables, drop_tables, SessionLocal
from app.db import crud

from random import randint


client = TestClient(app)


class TestItemAPI(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        create_tables()

    @classmethod
    def tearDownClass(cls):
        drop_tables()

    def setUp(self):
        """
        초기화 작업: 10개의 Item을 생성하고, ID 리스트를 얻습니다.
        """
        self.db = SessionLocal()  # 데이터베이스 세션을 초기화합니다.
        
        # 10개의 아이템을 생성합니다.
        self.num_items = 10
        self.items = [ItemCreate(name=f"Item {i}", description=f"Description {i}") for i in range(self.num_items)]
        self.id_list = []

        for item in self.items:
            db_item = crud.create_item(self.db, item)
            self.id_list.append(db_item.id)

        # 생성된 아이템 수 확인
        self.assertEqual(len(self.id_list), self.num_items)

    def tearDown(self):
        """
        데이터 정리: 모든 데이터 삭제.
        """
        for item_id in self.id_list:
            crud.delete_item(self.db, item_id)
        # 데이터베이스 세션을 종료합니다.
        self.db.close()

    def test_read_first_last_item(self):
        """
        생성된 첫 번째 및 마지막 Item을 조회합니다.
        """
        first_id = self.id_list[0]
        last_id = self.id_list[-1]

        # 첫 번째 Item 조회
        response = client.get(f"/api/v1/items/{first_id}")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.json()["description"], self.items[0].description)

        # 마지막 Item 조회
        response = client.get(f"/api/v1/items/{last_id}")
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.json()["description"], self.items[-1].description)

    def test_update_last_item(self):
        """
        마지막 Item의 description을 변경하고 일치하는지 확인합니다.
        """
        last_id = self.id_list[-1]
        new_description = "Updated Description"

        response = client.put(
            f"/api/v1/items/{last_id}",
            json={"description": new_description}
        )
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.json()["description"], new_description)

    def test_delete_random_items(self):
        """
        첫 번째 아이템부터 랜덤한 개수의 아이템을 삭제하고 남은 개수를 확인합니다.
        """
        num_to_delete = randint(1, 5)
        id_list_to_delete = self.id_list[:num_to_delete]

        for item_id in id_list_to_delete:
            response = client.delete(f"/api/v1/items/{item_id}")
            self.assertEqual(response.status_code, status.HTTP_200_OK)

        # 남은 아이템 개수 확인
        response = client.get("/api/v1/items/")
        remaining_items = response.json()
        self.assertEqual(len(remaining_items), self.num_items - num_to_delete)

if __name__ == "__main__":
    unittest.main()

 

 

Code를 보시면 CRUD Test중에 Create는 setUp method에 정의가 된 것을 알 수 있고, 나머지 Read, Update, Delete를 3개의 test method로 작성한 것을 보실 수 있습니다.

 

unittest code를 작성할 때 몇가지 특수 method를 알면 이해하기 더욱 쉽습니다.

  • setUpClass : Test Class가 생성될 때 동작하는 method입니다. 저는 여기서 table 생성을 작성했습니다.
  • tearDownClass : Test Class가 종료될 때 동작하는 method입니다. 저는 여기서 table 삭제를 작성했습니다.
  • setUp : test할 method의 시작하기 전에 할 동작. 저는 여기서 item을 생성했습니다.
  • tearDown : test할 method의 test가 끝나고 난 후 할 동작. 저는 여기서 item을 전부 삭제했습니다.
  • test_* : test할 동작을 작성하는 test method입니다. test_로 시작하는 method는 무작위 순서로 test가 진행됩니다.

 

실행을 해보면 다음과 같은 결과를 얻을 수 있습니다. Test 3개를 전부 통과했습니다.

OK를 얻을 수 있었다.

 

이 코드는 앞에서 작성할 때는 test.py에 작성해서 실행했었는데 이 unittest code 내용을 app/test/test_v1.py에 옮기도록 하겠습니다. 그리고 test_main.py를 작성해서 app/test/test_*.py에 속하는 다양한 test를 진행하도록 합시다.

 

import unittest

if __name__ == "__main__":
    # test 디렉토리에서 test_*.py 파일을 자동으로 찾습니다.
    loader = unittest.TestLoader()
    suite = loader.discover('app/test', pattern='test_*.py')

    # 테스트 실행
    runner = unittest.TextTestRunner()
    runner.run(suite)

 

 

 

이렇게 unit test도 끝났습니다. 앞으로 item과 관련된 기능을 추가할 때는 이 test.py에 작성하면 됩니다.

지금쯤이면 code가 어떤 구조로 되어 있을지 궁금하실 것 같아 구조를 보여 드립니다.

 

 


Outro

어느새 기본 기능을 만들고 unit test까지 만들고 통과했습니다. 이제서야 이 이미지를 충족하는 코드가 완성되었습니다.

아직 갈 길이 멀다.

 

다음시간에는 git을 활용해서 AWS Codecommit에 Code 배포를 해보도록 하겠습니다. 감사합니다.

 

 

 

* reference

https://docs.pytest.org/en/stable/

https://docs.python.org/ko/3/library/unittest.html