Python에서 pytest를 사용한 예외 및 오류 테스트 완벽 가이드

소프트웨어 개발에서 예외 및 오류 처리는 매우 중요합니다. 적절한 테스트를 통해 코드의 신뢰성을 높이고, 예기치 않은 오류로 인한 장애를 방지할 수 있습니다. 이 글에서는 Python의 테스트 프레임워크인 pytest를 사용하여 예외와 오류 처리를 효율적으로 테스트하는 방법을 자세히 설명합니다. 기본적인 설정부터 사용자 정의 예외 및 여러 예외 처리 테스트까지 단계적으로 설명하겠습니다.

목차

pytest의 기본 설정

pytest는 Python의 강력한 테스트 프레임워크로, 쉽게 설치하고 사용할 수 있습니다. 아래의 절차를 따라 pytest를 설정해 보겠습니다.

pytest 설치

먼저, pytest를 설치해야 합니다. 아래 명령어를 사용하여 pip을 통해 설치할 수 있습니다.

pip install pytest

기본 디렉토리 구조

프로젝트의 테스트 디렉토리 구조는 다음과 같이 설정하는 것을 권장합니다.

my_project/
├── src/
│   └── my_module.py
└── tests/
    ├── __init__.py
    └── test_my_module.py

첫 번째 테스트 파일 생성

다음으로, 테스트용 Python 파일을 생성합니다. 예를 들어, test_my_module.py라는 파일을 tests 디렉토리에 생성하고 아래 내용을 작성합니다.

def test_example():
    assert 1 + 1 == 2

테스트 실행

테스트를 실행하려면 프로젝트의 루트 디렉토리에서 다음 명령어를 실행합니다.

pytest

이를 통해 pytest가 자동으로 tests 디렉토리 내의 테스트 파일을 감지하고 실행합니다. 테스트가 성공적으로 완료되면 기본 설정이 완료된 것입니다.

예외 테스트 방법

예외가 올바르게 발생하는지 확인하기 위해 pytest에는 매우 유용한 방법이 준비되어 있습니다. 여기서는 특정 예외가 발생하는지 테스트하는 방법을 설명합니다.

pytest.raises를 사용한 예외 테스트

예외가 발생하는지 확인하려면 pytest.raises 컨텍스트 매니저를 사용합니다. 다음 예제에서는 제로 나눗셈이 발생하는지 테스트합니다.

import pytest

def test_zero_division():
    with pytest.raises(ZeroDivisionError):
        1 / 0

이 테스트에서는 1 / 0이 실행될 때 ZeroDivisionError가 발생하는지 확인합니다.

예외 메시지 확인

예외가 발생하는 것뿐만 아니라 특정 오류 메시지가 포함되어 있는지 확인할 수 있습니다. 이 경우 match 매개변수를 사용합니다.

def test_zero_division_message():
    with pytest.raises(ZeroDivisionError, match="division by zero"):
        1 / 0

이 테스트에서는 ZeroDivisionError가 발생하고 오류 메시지에 “division by zero”가 포함되어 있는지 확인합니다.

여러 예외 테스트하기

하나의 테스트 케이스에서 여러 예외를 테스트할 수도 있습니다. 예를 들어, 다른 조건에서 다른 예외가 발생하는지 확인할 수 있습니다.

def test_multiple_exceptions():
    with pytest.raises(ZeroDivisionError):
        1 / 0

    with pytest.raises(TypeError):
        '1' + 1

이 테스트에서는 먼저 ZeroDivisionError가 발생하는지 확인하고, 다음으로 TypeError가 발생하는지 확인합니다.

예외 테스트를 적절히 수행함으로써 코드가 예기치 않은 동작을 하지 않고, 올바른 오류 처리를 할 수 있도록 할 수 있습니다.

오류 메시지 검증

테스트에서 특정 오류 메시지가 포함되어 있는지 확인하는 것은 중요합니다. pytest를 사용하여 오류 메시지를 검증하는 방법에 대해 설명합니다.

특정 오류 메시지 확인하기

특정 예외가 발생할 뿐만 아니라 그 예외가 특정 오류 메시지를 포함하는지 테스트할 수 있습니다. pytest.raises를 사용하여 match 매개변수에 오류 메시지를 지정합니다.

import pytest

def test_value_error_message():
    def raise_value_error():
        raise ValueError("This is a ValueError with a specific message.")

    with pytest.raises(ValueError, match="specific message"):
        raise_value_error()

이 테스트에서는 ValueError가 발생하고, 그 메시지에 “specific message”가 포함되어 있는지 확인합니다.

정규 표현식을 사용한 오류 메시지 검증

오류 메시지가 동적으로 생성되거나 부분 일치를 확인하고 싶은 경우 정규 표현식을 사용할 수 있습니다.

def test_regex_error_message():
    def raise_type_error():
        raise TypeError("TypeError: invalid type for operation")

    with pytest.raises(TypeError, match=r"invalid type"):
        raise_type_error()

이 테스트에서는 TypeError의 메시지에 “invalid type”이라는 문구가 포함되어 있는지 확인합니다. match 매개변수에 정규 표현식을 전달하여 부분 일치 검증이 가능합니다.

사용자 정의 오류 메시지 검증

직접 정의한 사용자 정의 예외에 대해서도 같은 방법으로 오류 메시지를 검증할 수 있습니다.

class CustomError(Exception):
    pass

def test_custom_error_message():
    def raise_custom_error():
        raise CustomError("This is a custom error message.")

    with pytest.raises(CustomError, match="custom error message"):
        raise_custom_error()

이 테스트에서는 CustomError가 발생하고, 그 메시지에 “custom error message”가 포함되어 있는지 확인합니다.

오류 메시지 검증은 사용자에게 일관된 오류 메시지를 제공하고, 디버깅 정보를 정확하게 보장하는 데 중요합니다. pytest를 활용하여 이러한 테스트를 효율적으로 수행합시다.

여러 예외 처리 테스트

하나의 함수가 여러 예외를 던질 가능성이 있는 경우, 각각의 예외가 올바르게 처리되는지 테스트하는 것이 중요합니다. pytest를 사용하여 다양한 예외 처리를 한 번에 테스트하는 방법을 소개합니다.

여러 예외를 한 번의 테스트로 확인하기

함수가 다른 입력에 대해 다른 예외를 던지는 경우, 각각의 예외가 적절히 처리되는지 확인하기 위한 테스트를 수행합니다.

import pytest

def error_prone_function(value):
    if value == 0:
        raise ValueError("Value cannot be zero")
    elif value < 0:
        raise TypeError("Value cannot be negative")
    return True

def test_multiple_exceptions():
    with pytest.raises(ValueError, match="Value cannot be zero"):
        error_prone_function(0)

    with pytest.raises(TypeError, match="Value cannot be negative"):
        error_prone_function(-1)

이 테스트에서는 error_prone_function이 0일 때 ValueError를, 음수일 때 TypeError를 던지는지 확인합니다.

파라미터화된 테스트로 예외 확인

같은 함수에 대해 다른 예외가 발생하는 경우 파라미터화된 테스트를 사용하여 효율적으로 테스트할 수 있습니다.

@pytest.mark.parametrize("value, expected_exception, match_text", [
    (0, ValueError, "Value cannot be zero"),
    (-1, TypeError, "Value cannot be negative")
])
def test_error_prone_function(value, expected_exception, match_text):
    with pytest.raises(expected_exception, match=match_text):
        error_prone_function(value)

이 파라미터화된 테스트에서는 value에 대해 예상되는 예외와 오류 메시지를 조합하여 테스트를 수행합니다.

사용자 정의 메서드를 사용한 여러 예외 테스트

여러 예외를 던지는 함수를 사용자 정의 메서드를 사용하여 효율적으로 테스트할 수도 있습니다.

def test_custom_multiple_exceptions():
    def assert_raises_with_message(func, exception, match_text):
        with pytest.raises(exception, match=match_text):
            func()

    assert_raises_with_message(lambda: error_prone_function(0), ValueError, "Value cannot be zero")
    assert_raises_with_message(lambda: error_prone_function(-1), TypeError, "Value cannot be negative")

이 테스트에서는 사용자 정의 메서드 assert_raises_with_message를 사용하여 함수가 특정 입력에서 올바른 예외와 오류 메시지를 던지는지 확인합니다.

여러 예외를 하나의 테스트로 한 번에 확인함으로써 테스트 코드의 중복을 줄이고 유지보수성을 향상시킬 수 있습니다. pytest의 기능을 활용하여 효율적으로 예외 처리 테스트를 수행합시다.

사용자 정의 예외 테스트

독자적으로 정의한 예외를 사용하면 애플리케이션의 오류 처리를 더욱 명확하게 하고, 특정 오류 상황에 대해 적절히 대응할 수 있습니다. 여기서는 사용자 정의 예외를 테스트하는 방법을 설명합니다.

사용자 정의 예외 정의

먼저, 사용자 정의 예외를 정의합니다. Python의 내장 예외 클래스를 상속하여 새로운 예외 클래스를 만듭니다.

class CustomError(Exception):
    """사용자 정의 예외의 기본 클래스"""
    pass

class SpecificError(CustomError):
    """특정 오류를 나타내는 사용자 정의 예외"""
    pass

사용자 정의 예외를 발생시키는 함수

다음으로, 특정 조건에서 사용자 정의 예외를 발생시키는 함수를 만듭니다.

def function_that_raises(value):
   

 if value == 'error':
        raise SpecificError("An error occurred with value: error")
    return True

사용자 정의 예외 테스트

pytest를 사용하여 사용자 정의 예외가 올바르게 발생하는지 테스트합니다.

import pytest

def test_specific_error():
    with pytest.raises(SpecificError, match="An error occurred with value: error"):
        function_that_raises('error')

이 테스트에서는 function_that_raises 함수가 SpecificError를 던지고, 그 오류 메시지가 예상대로인지 확인합니다.

여러 사용자 정의 예외 테스트

여러 사용자 정의 예외를 사용하는 경우 각각의 예외가 올바르게 처리되는지 테스트합니다.

class AnotherCustomError(Exception):
    """다른 사용자 정의 예외"""
    pass

def function_with_multiple_custom_errors(value):
    if value == 'first':
        raise SpecificError("First error occurred")
    elif value == 'second':
        raise AnotherCustomError("Second error occurred")
    return True

def test_multiple_custom_errors():
    with pytest.raises(SpecificError, match="First error occurred"):
        function_with_multiple_custom_errors('first')

    with pytest.raises(AnotherCustomError, match="Second error occurred"):
        function_with_multiple_custom_errors('second')

이 테스트에서는 function_with_multiple_custom_errors 함수가 다른 입력 값에 대해 올바른 사용자 정의 예외를 던지는지 확인합니다.

사용자 정의 예외 메시지 확인

사용자 정의 예외의 오류 메시지가 올바른지 확인하는 것도 중요합니다.

def test_custom_error_message():
    with pytest.raises(SpecificError, match="An error occurred with value: error"):
        function_that_raises('error')

이 테스트에서는 SpecificError가 발생하고, 그 오류 메시지에 “An error occurred with value: error”가 포함되어 있는지 확인합니다.

사용자 정의 예외 테스트를 통해 애플리케이션이 특정 오류 상황을 적절히 처리할 수 있도록 보장하고, 코드의 품질과 신뢰성을 높일 수 있습니다.

응용 예제: API 오류 테스트

API를 개발할 때 오류 처리는 매우 중요합니다. 클라이언트가 적절한 오류 메시지를 수신할 수 있도록 하려면 오류 테스트를 수행해야 합니다. 여기서는 pytest를 사용하여 API의 오류 처리를 테스트하는 방법을 소개합니다.

FastAPI를 사용한 API 예제

먼저, 간단한 FastAPI 엔드포인트를 정의합니다. 이 예제에서는 특정 오류가 발생하는 엔드포인트를 생성합니다.

from fastapi import FastAPI, HTTPException

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id == 0:
        raise HTTPException(status_code=400, detail="Item ID cannot be zero")
    if item_id < 0:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item_id": item_id, "name": "Item Name"}

이 엔드포인트는 item_id가 0일 때는 400 오류, 음수일 때는 404 오류를 반환합니다.

pytest와 httpx를 사용한 오류 테스트

다음으로, pytest와 httpx를 사용하여 API 오류 테스트를 수행합니다.

import pytest
from httpx import AsyncClient
from main import app

@pytest.mark.asyncio
async def test_read_item_zero():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        response = await ac.get("/items/0")
    assert response.status_code == 400
    assert response.json() == {"detail": "Item ID cannot be zero"}

@pytest.mark.asyncio
async def test_read_item_negative():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        response = await ac.get("/items/-1")
    assert response.status_code == 404
    assert response.json() == {"detail": "Item not found"}

이 테스트에서는 /items/0에 요청을 보내 400 오류와 특정 오류 메시지를 확인합니다. 또한 /items/-1에 요청을 보내 404 오류와 오류 메시지를 확인합니다.

오류 테스트의 자동화 및 효율화

파라미터화된 테스트를 사용하여 다양한 오류 케이스를 하나의 테스트에 통합할 수 있습니다.

@pytest.mark.asyncio
@pytest.mark.parametrize("item_id, expected_status, expected_detail", [
    (0, 400, "Item ID cannot be zero"),
    (-1, 404, "Item not found"),
])
async def test_read_item_errors(item_id, expected_status, expected_detail):
    async with AsyncClient(app=app, base_url="http://test") as ac:
        response = await ac.get(f"/items/{item_id}")
    assert response.status_code == expected_status
    assert response.json() == {"detail": expected_detail}

이 파라미터화된 테스트에서는 다양한 item_id에 대해 예상되는 상태 코드와 오류 메시지를 검증합니다. 이를 통해 테스트 코드를 간결하고 효율적으로 유지할 수 있습니다.

API 오류 테스트를 통해 클라이언트가 올바른 오류 메시지를 수신하고 애플리케이션의 신뢰성을 높일 수 있습니다.

pytest의 픽스처를 활용한 오류 테스트

픽스처는 테스트의 설정과 정리를 효율적으로 수행하기 위한 pytest의 강력한 기능입니다. 오류 테스트에 픽스처를 활용하여 코드의 재사용성과 가독성을 높일 수 있습니다.

픽스처 기본

먼저, pytest 픽스처의 기본 사용 방법을 설명합니다. 픽스처는 공통 준비 작업을 모아서 정의하고 여러 테스트에서 사용할 수 있습니다.

import pytest

@pytest.fixture
def sample_data():
    return {"name": "test", "value": 42}

def test_sample_data(sample_data):
    assert sample_data["name"] == "test"
    assert sample_data["value"] == 42

이 예제에서는 sample_data라는 픽스처를 정의하고, 이를 테스트 함수에서 사용하고 있습니다.

API 설정에 픽스처 사용

API 테스트의 경우, 픽스처를 사용하여 클라이언트를 설정할 수 있습니다. 아래 예제에서는 httpx의 AsyncClient를 픽스처로 설정합니다.

import pytest
from httpx import AsyncClient
from main import app

@pytest.fixture
async def async_client():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        yield ac

@pytest.mark.asyncio
async def test_read_item_zero(async_client):
    response = await async_client.get("/items/0")
    assert response.status_code == 400
    assert response.json() == {"detail": "Item ID cannot be zero"}

이 테스트에서는 async_client 픽스처를 사용하여 클라이언트를 설정하고, API 요청을 보냅니다.

여러 오류 테스트에 픽스처 활용

픽스처를 활용하여 여러 오류 테스트를 효율적으로 수행할 수 있습니다.

@pytest.mark.asyncio
@pytest.mark.parametrize("item_id, expected_status, expected_detail", [
    (0, 400, "Item ID cannot be zero"),
    (-1, 404, "Item not found"),
])
async def test_read_item_errors(async_client, item_id, expected_status, expected_detail):
    response = await async_client.get(f"/items/{item_id}")
    assert response.status_code == expected_status
    assert response.json() == {"detail": expected_detail}

이 예제에서는 async_client 픽스처와 파라미터화된 테스트를 결합하여 다양한 오류 테스트 케이스를 간단하게 작성합니다.

데이터베이스 연결 픽스처

데이터베이스 연결이 필요한 테스트의 경우, 픽스처를 사용하여 연결의 설정과 정리를 수행할 수 있습니다.

import sqlalchemy
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite+aiosqlite:///./test.db"

@pytest.fixture
async def async_db_session():
    engine = create_async_engine(DATABASE_URL, echo=True)
    async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
    async with async_session() as session:
        yield session
    await engine.dispose()

async def test_db_interaction(async_db_session):
    result = await async_db_session.execute("SELECT 1")
    assert result.scalar() == 1

이 예제에서는 async_db_session 픽스처를 사용하여 데이터베이스 연결을 관리하고, 테스트 후에 정리를 수행합니다.

픽스처를 활용함으로써 테스트 코드의 중복을 줄이고, 테스트 유지 관리를 쉽게 할 수 있습니다. pytest의 픽스처를 잘 활용하여 효율적인 오류 테스트를 실현합시다.

요약

pytest를 사용한 예외 및 오류 테스트는 소프트웨어의 품질을 향상하는 데 필수적입니다. 이 글에서는 기본 설정부터 시작하여 예외 테스트 방법, 오류 메시지 검증, 여러 예외 처리 테스트, 사용자 정의 예외 테스트, API 오류 테스트, 그리고 픽스처를 활용한 오류 테스트에 대해 자세히 설명했습니다.

pytest의 기능을 최대한 활용함으로써 오류 처리를 효율적이고 효과적으로 테스트할 수 있습니다. 이를 통해 코드의 신뢰성과 유지 보수성이 향상되며, 예기치 않은 오류로 인한 문제를 미리 방지할 수 있습니다. 앞으로도 pytest를 사용한 테스트 기법을 배우고, 보다 높은 품질의 소프트웨어 개발을 목표로 합시다.

목차