pytest 란?:

  • 파이썬에서 가장 널리 사용되는 테스트 프레임워크 중 하나
  • 특징:
    • 간단한 문법으로 쉽게 시작 가능
    • 테스트 자동 검색(Discovery) 기능: 특별한 설정 없이도 test_*.py 혹은 *_test.py 형식의 파일을 자동으로 찾음
    • 고급 기능: Fixtures, parameterized tests 등
  • 파이썬 표준 라이브러리에서 제공하는 unittest 모듈보다 더 직관적이고 유연하다는 평가를 받습니다.

 

설치 및 기본 실행:

# 설치 
pip install pytest

 

pytest 디렉토리 구조 예시:

  • 테스트 코드는 tests/ 디렉터리에 모아두는 것이 일반적
  • 파일 이름은 test_*.py 형태로 하면 pytest가 자동으로 찾아줌
my_project/
├── app/
│   ├── __init__.py
│   └── main.py
├── tests/
│   ├── __init__.py
│   └── test_main.py
└── requirements.txt

 

테스트 실행:

  • 별도의 인수가 없으면 현재 디렉터리와 하위 디렉터리를 탐색하여 test_*.py 파일을 찾아 실행합니다.
# 프로젝트 루트 디렉터리에서
pytest

 

가장 기본적인 테스트 작성 예시:

  • 테스트 함수 명 규칙: test_로 시작해야 함
  • Assertion: assert 구문 사용. assert <조건>, <에러메시지(optional)>
  • 파이썬에서는 unittest처럼 self.assertEqual(…) 등을 안 써도 되고, 기본 제공되는 assert문을 써도 충분히 자세한 에러 메시지를 볼 수 있습니
    다.
    # tests/test_main.py
    def test_sum():
        assert 1 + 2 == 3
    
    def test_uppercase():
        assert "hello".upper() == "HELLO"

 

테스트 구조화: 클래스나 모듈별로 나누기: 

  • 규모가 커지면 테스트를 클래스 단위로 구분하기도 합니다.
  • 클래스 이름은 Test로 시작하도록 규칙을 정해 두면 가독성이 높습니다.
  • 클래스 내부에서도 함수 이름을 test_로 시작해야 pytest가 테스트 함수로 인식합니다.
# tests/test_user.py

class TestUserCreation:
    def test_create_user_success(self):
        user = create_user("Alice")
        assert user.name == "Alice"

    def test_create_user_failure(self):
        # 예: 중복된 이름일 때
        with pytest.raises(ValueError):
            create_user("Alice")  # 이미 있는 유저라 실패

 

예외 상황 테스트:

  • 테스트하다 보면 특정 함수가 예외를 던지는지 여부를 검증해야 할 때가 있습니다. pytest는 pytest.raises를 통해 예외 상황을 테스트할 수 있습니다.
def test_raise_exception():
    with pytest.raises(ValueError) as exc_info:
        raise ValueError("Custom error")

    assert "Custom error" in str(exc_info.value)
@patch('requests.get')
def test_resource_cleanup(self, mock_get):
    mock_get.side_effect = requests.RequestException
    with pytest.raises(requests.RequestException):
        crawler.extract(test_url)
    # 리소스가 적절히 정리되었는지 확인
    assert crawler._is_cleaned_up()

 

 

Mocking (단위 테스트를 위한 의존성 제어):

  • 다른 언어에서도 테스트 시 의존성을 제거하거나, 외부 API를 가짜로 대체하기 위해 mocking을 자주 사용합니다. 파이썬에서도 unittest.mock 라이브러리를 자주 쓰며, pytest는 이를 매끄럽게 연동합니다.
from unittest.mock import patch

def test_third_party_call():
    with patch("app.main.some_third_party_function") as mock_func:
        mock_func.return_value = "mocked!"

        result = do_something()  # 내부적으로 some_third_party_function이 호출됨
        assert result == "mocked!"
        mock_func.assert_called_once()

 

Coverage(테스트 커버리지) 확인하기:

  • 프로젝트에서 테스트 커버리지를 확인하고 싶다면 coverage 패키지와 연동해서 사용할 수 있습니다.
# 설치 
pip install coverage

# 실행 
coverage run -m pytest
coverage report -m
# 혹은
coverage html  # HTML 리포트 생성

 

자주 쓰는 플러그인

  • pytest-django: Django 프로젝트 테스트 지원
  • pytest-flask: Flask, FastAPI 등에서도 유사하게 사용할 수 있는 플러그인 존재
  • pytest-cov: coverage와의 통합, --cov 옵션을 제공
  • pytest-xdist: 테스트를 멀티프로세싱으로 병렬 실행

 

@pytest.fixture

테스트할 때 초기화 작업(예: DB 연결, 임시 파일 생성 등)이 필요하거나, 테스트가 끝난 뒤 정리 작업이 필요할 수 있습니다. pytest에서는 이를 손쉽게 처리하기 위해 Fixture라는 기능을 제공합니다.

 

기본 사용법:

class TestFmKoreaCrawler:
    @pytest.fixture
    def crawler(self):
        return FmKoreaCrawler()

    def test_extract_title(self, crawler):  # fixture를 매개변수로 자동 주입
        result = crawler.extract_title()
        assert result == "expected title"

 

설정과 정리:

@pytest.fixture
def database_connection():
    # 설정 (Setup)
    db = connect_to_db()

    yield db  # 테스트에서 사용할 객체 반환

    # 정리 (Teardown)
    db.close()

 

설정과 정리 - 데이터베이스 트랜잭션:

@pytest.fixture
def transaction():
    # Setup
    connection = create_connection()
    transaction = connection.begin()

    yield connection  # 테스트에 연결 전달

    # Teardown (항상 롤백)
    transaction.rollback()
    connection.close()

 

fixture 범위 지정:

@pytest.fixture(scope="function")  # 각 테스트 함수마다 새로 생성
def new_crawler():
    return FmKoreaCrawler()

@pytest.fixture(scope="class")     # 테스트 클래스당 한 번만 생성
def shared_crawler():
    return FmKoreaCrawler()

@pytest.fixture(scope="session")   # 전체 테스트 세션에서 한 번만 생성
def config():
    return load_config()

 

fixture 조합:

@pytest.fixture
def mock_response():
    return """<html><body>Test</body></html>"""

@pytest.fixture
def crawler_with_mock(crawler, mock_response):  # 다른 fixture 사용
    crawler.response = mock_response
    return crawler

 

매개변수화된 fixture:

@pytest.fixture(params=['success', 'error'])
def test_scenario(request):
    return request.param

def test_crawler(test_scenario):
    if test_scenario == 'success':
        # 성공 케이스 테스트
        pass
    else:
        # 에러 케이스 테스트
        pass

 

 

Parametrizing: 한 번의 함수로 다양한 케이스 테스트

테스트 케이스가 여러 개인 상황(예: 여러 개의 입력 조합을 모두 테스트)에서, 비슷한 구조의 테스트 함수를 여러 번 복붙하고 싶지 않을 때 유용합니다.

import pytest

@pytest.mark.parametrize("input_val,expected", [
    (1, 2),
    (2, 4),
    (3, 6),
])
def test_double(input_val, expected):
    assert input_val * 2 == expected

 

vs fixture 에서 parmse=[]:

  • 둘 다 테스트를 여러 입력 값으로 반복 실행한다는 점에서 유사하지만, 사용 목적과 적용 범위가 다릅니다
  • Parametrizing 은 테스트 함수 레벨에서 직접 값을 제공함
  • Parametrizing 은 다중 인자를 쉽게 파라미터화 가능
  • 테스트 케이스별 의미를 명시할 수 있음.

'Programming Language > Python' 카테고리의 다른 글

제너레이터(generator)  (0) 2025.02.07
Asyncio 와 Aiohttp 정리  (0) 2024.08.14
Jupyter notebook 에서 argparse 를 사용할 때 발생하는 에러  (0) 2024.08.07
yield 에 대해서  (0) 2024.08.06
언패킹 (Unpacking)  (0) 2024.07.10

+ Recent posts