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 }")
중첩 조롱 구조 이해
여기서 주요 통찰력은 중첩 된 컨텍스트 관리자 패턴을 복제하기 위해 모의를 구성하는 방법입니다.
mock_response
최종 컨텍스트 관리자가 반환 한 가장 안쪽 객체입니다.async_mock_response
비동기 컨텍스트 관리자로 랩합니다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 }")
비동기 컨텍스트 관리자 조롱에 대한 모범 사례
실제 경험을 바탕으로 다음과 같은 모범 사례가 있습니다.
- 재사용 가능한 비품을 만듭니다. 반복적 인 코드를 피하기 위해 일반적인 모의 패턴에 대한 pytest 비품을 정의하십시오.
- 설명 이름을 사용하십시오. 중첩 된 모의를 다룰 때 명확한 이름 지정이 필수적입니다.
- 모의 상호 작용을 확인하십시오. 사용
assert_called_with()
당신의 모의가 예상되는 주장과 함께 호출되었는지 확인합니다. - 오류 경로를 철저히 테스트합니다. 행복한 길을 테스트하지 마십시오. 오류 처리가 올바르게 작동하는지 확인하십시오.
- 모의 체인을 가능한 한 얕게 유지하십시오. 모의 체인이 깊을수록 이해하고 유지하기가 더 어려워집니다.
피해야 할 일반적인 함정
비동기 컨텍스트 관리자를 조롱 할 때 이러한 일반적인 문제를 조심하십시오.
- 제대로 구현되지 않습니다
__aenter__
그리고__aexit__
. 비동기 컨텍스트 관리자가 작동하려면 두 방법 모두 올바르게 구현해야합니다. - 모의 방법을 비동기로 만드는 것을 잊어 버립니다. 기다리고있는 모든 방법은 비동기로 올바르게 조롱해야합니다.
- 잘못된 중첩 순서. 모의 구조가 테스트중인 코드의 중첩 순서와 일치하는지 확인하십시오.
- 누락 된 응답 방법 조롱. 응답 객체에서 호출 된 모든 메소드를 조롱해야합니다.
결론
중첩 컨텍스트 관리자로 비동기 코드를 테스트하는 것은 어려울 수 있지만 AsyncContextManagerMock
접근법은 이러한 코드를 효과적으로 테스트하기위한 깨끗하고 재사용 가능한 패턴을 제공합니다. 이 기술은 테스트 가독성을 유지하고 복잡한 모의 설정을 피하면서 비동기 클라이언트 라이브러리의 포괄적 인 테스트를 가능하게합니다.
이러한 패턴을 적용하면 중첩 컨텍스트 관리자와 관련된 가장 복잡한 비동기 작업에 대한 강력한 테스트를 만들 수 있습니다. 결과는 비동기 파이썬 응용 프로그램에 대한 철저한 테스트 범위를 갖춘보다 안정적인 코드베이스입니다.
Post Comment