Python 테스트에서 Async Context Manager 마스터 링

Python 테스트에서 Async Context Manager 마스터 링

비동기식 파이썬 코드를 테스트하는 것은 특히 중첩 컨텍스트 관리자를 다룰 때 어려울 수 있습니다. 이 튜토리얼에서는 중첩 된 비동기 컨텍스트 관리자를 효과적으로 조롱하여 복잡한 비동기 코드 구조에 대한 깨끗하고 신뢰할 수있는 테스트를 만드는 방법을 살펴 보겠습니다.

중첩 된 비동기 컨텍스트 관리자 테스트의 과제

현대의 파이썬 코드베이스는 종종 비동기 컨텍스트 관리자를 사용합니다 (사용 async with) Async 함수의 리소스 관리 용. 이러한 상황 관리자가 중첩되면 테스트가 복잡해집니다. 중첩 컨텍스트 관리자를 사용하여 HTTP 세션 및 요청을 관리하는 클라이언트를 고려하십시오.

async def _execute(self, method, query, variables=None):
    async with self.get_session() as session:
        async with session.request(method, self.api_url, data=json.dumps({
            "query": query, 
            "variables": variables or {}
        })) as response:
            # Process response
            result = await response.json()
            return result

이를 제대로 테스트하려면 두 컨텍스트 관리자를 조롱해야합니다. 사용하는 전통적인 접근법 unittest.mock.AsyncMock 중첩 된 비동기 컨텍스트와 함께 빠르게 복잡해집니다.

더 나은 솔루션 : AsyncContextManagerMock

비동기 컨텍스트 관리자의 동작을 시뮬레이션하는 특수 모의를 만들 수 있습니다.

class AsyncContextManagerMock:
    """Mock for async context managers with nested mocking capabilities."""
    def __init__(self, mock):
            """Initialize with a mock that will be returned from __aenter__."""
            self.mock = mock
        async def __aenter__(self):
            """Enter async context manager."""
            return self.mock
        async def __aexit__(self, exc_type, exc, tb):
            """Exit async context manager."""
            pass
        def request(self, *args, **kwargs):
            """Return mock to support chaining."""
            return self.mock

이 클래스는 Async Context Manager와 같이 작동하는 모의 개체를 만드는 데 사용될 수 있으며, 사용시 구성된 모의 개체를 반환합니다. async with.

실제 예 : GraphQL 클라이언트 테스트

이 기술을 보여주기 위해 단순화 된 GraphQL 클라이언트 클래스를 만들어 봅시다.

class GraphQLClient:
    """A simple async GraphQL client."""
    
    def __init__(self, base_url):
        """Initialize with the API base URL."""
        self.base_url = base_url
        self.api_url = f"{base_url}/graphql"
    
    def get_session(self):
        """Get aiohttp ClientSession."""
        # In a real implementation, this might use a connection pool or session cache
        return aiohttp.ClientSession()
    
    async def query(self, query_string, variables=None):
        """Execute a GraphQL query."""
        payload = {
            "query": query_string,
            "variables": variables or {}
        }
        
        async with self.get_session() as session:
            async with session.request(
                "POST", 
                self.api_url,
                json=payload,
                headers={"Content-Type": "application/json"}
            ) as response:
                response.raise_for_status()
                result = await response.json()
                
                if "errors" in result:
                    raise GraphQLError(f"Query failed: {result['errors']}")
                    
                return result["data"]

AsyncContextManagerMock으로 효과적인 테스트 작성

이제 AsyncContextManagerMock:

import pytest
from unittest.mock import AsyncMock, patch

@pytest.mark.asyncio
async def test_successful_query():
    """Test successful GraphQL query execution."""
    # Create mock response with successful data
    mock_response = AsyncMock()
    mock_response.json.return_value = {"data": {"test": "value"}}
    
    # Create nested async context manager mocks
    async_mock_response = AsyncContextManagerMock(mock=mock_response)
    mock_session = AsyncContextManagerMock(
        mock=AsyncContextManagerMock(mock=async_mock_response)
    )
    
    # Create client and patch the get_session method
    client = GraphQLClient("
    with patch.object(client, "get_session", return_value=mock_session):
        result = await client.query("query { test }", {"var": "value"})
        
        assert result == {"test": "value"}
@pytest.mark.asyncio
async def test_query_with_errors():
    """Test GraphQL query that returns errors."""
    # Create mock response with GraphQL errors
    mock_response = AsyncMock()
    mock_response.json.return_value = {"errors": ["Error message"]}
    
    # Create nested async context manager mocks
    async_mock_response = AsyncContextManagerMock(mock=mock_response)
    mock_session = AsyncContextManagerMock(
        mock=AsyncContextManagerMock(mock=async_mock_response)
    )
    
    # Create client and patch the get_session method
    client = GraphQLClient("
    with patch.object(client, "get_session", return_value=mock_session):
        with pytest.raises(GraphQLError):
            await client.query("query { test }")

중첩 조롱 구조 이해

여기서 주요 통찰력은 중첩 된 컨텍스트 관리자 패턴을 복제하기 위해 모의를 구성하는 방법입니다.

  1. mock_response 최종 컨텍스트 관리자가 반환 한 가장 안쪽 객체입니다.
  2. async_mock_response 비동기 컨텍스트 관리자로 랩합니다
  3. mock_session 돌아올 다른 컨텍스트 관리자를 랩핑합니다 async_mock_response

이것은 생산 코드의 정확한 구조를 모방하는 체인을 만듭니다.

async with session as session:
    async with session.request(...) as response:
        # Use response

고급 사용 : 추가 테스트 사례

그만큼 AsyncContextManagerMock 접근 방식은 다양한 테스트 시나리오를 처리 할 수있을 정도로 유연합니다. 다음은 몇 가지 추가 예입니다.

HTTP 오류 테스트

@pytest.mark.asyncio
async def test_http_error():
    """Test handling of HTTP errors."""
    mock_response = AsyncMock()
    mock_response.raise_for_status.side_effect = aiohttp.ClientError("HTTP Error")
    
    async_mock_response = AsyncContextManagerMock(mock=mock_response)
    mock_session = AsyncContextManagerMock(
        mock=AsyncContextManagerMock(mock=async_mock_response)
    )
    
    client = GraphQLClient("
    with patch.object(client, "get_session", return_value=mock_session):
        with pytest.raises(aiohttp.ClientError):
            await client.query("query { test }")

네트워크 시간 초과 테스트

@pytest.mark.asyncio
async def test_timeout():
    """Test handling of network timeouts."""
    mock_session = AsyncContextManagerMock(mock=AsyncMock())
    mock_session.mock.request.side_effect = asyncio.TimeoutError()
    
    client = GraphQLClient("
    with patch.object(client, "get_session", return_value=mock_session):
        with pytest.raises(asyncio.TimeoutError):
            await client.query("query { test }")

비동기 컨텍스트 관리자 조롱에 대한 모범 사례

실제 경험을 바탕으로 다음과 같은 모범 사례가 있습니다.

  1. 재사용 가능한 비품을 만듭니다. 반복적 인 코드를 피하기 위해 일반적인 모의 패턴에 대한 pytest 비품을 정의하십시오.
  2. 설명 이름을 사용하십시오. 중첩 된 모의를 다룰 때 명확한 이름 지정이 필수적입니다.
  3. 모의 상호 작용을 확인하십시오. 사용 assert_called_with() 당신의 모의가 예상되는 주장과 함께 호출되었는지 확인합니다.
  4. 오류 경로를 철저히 테스트합니다. 행복한 길을 테스트하지 마십시오. 오류 처리가 올바르게 작동하는지 확인하십시오.
  5. 모의 체인을 가능한 한 얕게 유지하십시오. 모의 체인이 깊을수록 이해하고 유지하기가 더 어려워집니다.

피해야 할 일반적인 함정

비동기 컨텍스트 관리자를 조롱 할 때 이러한 일반적인 문제를 조심하십시오.

  1. 제대로 구현되지 않습니다 __aenter__ 그리고 __aexit__. 비동기 컨텍스트 관리자가 작동하려면 두 방법 모두 올바르게 구현해야합니다.
  2. 모의 방법을 비동기로 만드는 것을 잊어 버립니다. 기다리고있는 모든 방법은 비동기로 올바르게 조롱해야합니다.
  3. 잘못된 중첩 순서. 모의 구조가 테스트중인 코드의 중첩 순서와 일치하는지 확인하십시오.
  4. 누락 된 응답 방법 조롱. 응답 객체에서 호출 된 모든 메소드를 조롱해야합니다.

결론

중첩 컨텍스트 관리자로 비동기 코드를 테스트하는 것은 어려울 수 있지만 AsyncContextManagerMock 접근법은 이러한 코드를 효과적으로 테스트하기위한 깨끗하고 재사용 가능한 패턴을 제공합니다. 이 기술은 테스트 가독성을 유지하고 복잡한 모의 설정을 피하면서 비동기 클라이언트 라이브러리의 포괄적 인 테스트를 가능하게합니다.

이러한 패턴을 적용하면 중첩 컨텍스트 관리자와 관련된 가장 복잡한 비동기 작업에 대한 강력한 테스트를 만들 수 있습니다. 결과는 비동기 파이썬 응용 프로그램에 대한 철저한 테스트 범위를 갖춘보다 안정적인 코드베이스입니다.

출처 참조

Post Comment

당신은 놓쳤을 수도 있습니다