Как работают модульные тесты с использованием (асинхронных) макетов объектов (pytest, unittest)Python

Программы на Python
Ответить
Anonymous
 Как работают модульные тесты с использованием (асинхронных) макетов объектов (pytest, unittest)

Сообщение Anonymous »

Поначалу я занимаюсь анализом данных, но мне было поручено провести модульные тесты для написанного нами кода. Короче говоря, это не моя сфера деятельности, и мне поручили этим заниматься. На данный момент у меня есть некоторый код, и мне хотелось подробного объяснения того, КАК он на самом деле работает, и некоторой уверенности в том, что то, что я сделал, правильно, а если нет, то как я действительно могу это сделать. Мне нужно объяснить это своим коллегам, но я сам немного озадачен тем, как работает эта библиотека.
Сначала давайте сосредоточимся на модульном тестировании конечной точки API. Вот конечная точка, при достижении которой будут извлечены все случаи из базы данных SQL для определенного агента_id:

Код: Выделить всё

@router.get("/cases", response_model=List[CaseInDB])
async def get_all_cases(
agent_id: str = Query(..., description="The ID of the agent to retrieve the cases for"),
db: Session = Depends(get_db)
):
return await case_service.get_all_cases(db, agent_id)
Теперь, чтобы выполнить модульный тест:

Код: Выделить всё

import pytest
from fastapi.testclient import TestClient
from unittest.mock import AsyncMock, patch, MagicMock
from datetime import datetime

from app.main import app
from app.schemas.case import CaseCreate, CaseUpdate, CaseInDB
from app.models.case import Case
from app.repository.database import CaseRepository
from app.services.case_service import CaseService

client = TestClient(app)

# Mock Data
MOCK_AGENT_ID = "test_xhuma"
MOCK_CASE_ID = "66666"
MOCK_CASE_DATA = {
"case_title": "Test Case",
"case_description": "Test Description",
"case_status": "Open",
"client_id": "client123",
"client_name": "John Doe",
"client_phone": "+1234567890",
"client_email": "john@example.com",
"client_city": "Test City",
"client_coords": "12.345,67.890",
"client_address": "123 Test St",
"case_diagnosis": "Initial diagnosis"
}

MOCK_CASE_ID_2 = "66667"
MOCK_CASE_DATA_2 = {
"case_title": "Test Case 2",
"case_description": "Test Description",
"case_status": "Open",
"client_id": "client123",
"client_name": "Jane Doe",
"client_phone": "+1234567890",
"client_email": "john@example.com",
"client_city": "Test City",
"client_coords": "12.345,67.890",
"client_address": "123 Test St",
"case_diagnosis": "Initial diagnosis"
}

MOCK_DB_CASE = Case(
agent_id=MOCK_AGENT_ID,
case_id=MOCK_CASE_ID,
created_at=datetime.now(),
updated_at=datetime.now(),
**MOCK_CASE_DATA
)

MOCK_DB_CASE_2 = Case(
agent_id=MOCK_AGENT_ID,
case_id=MOCK_CASE_ID_2,
created_at=datetime.now(),
updated_at=datetime.now(),
**MOCK_CASE_DATA_2
)

@pytest.fixture
def mock_case_service():
with patch('app.api.cases.case_service') as mock:
yield mock

@pytest.fixture
def mock_db():
with patch('app.api.cases.get_db') as mock:
yield mock

@pytest.fixture
def mock_repository():
return MagicMock(spec=CaseRepository)

@pytest.fixture
def case_service(mock_repository):
return CaseService(mock_repository)

@pytest.fixture
def mock_redis_publisher():
with patch('app.services.case_service.publish_to_stream', new_callable=AsyncMock) as mock:
yield mock

class TestCaseAPI:

def test_get_all_cases(self, mock_case_service, mock_db):

mock_cases = [MOCK_DB_CASE, MOCK_DB_CASE_2]
mock_case_service.get_all_cases = AsyncMock(return_value=[CaseInDB.model_validate(case) for case in mock_cases])

response = client.get(f"/cases?agent_id={MOCK_AGENT_ID}")

assert response.status_code == 200
assert len(response.json()) == 2
assert response.json()[0]["case_id"] == MOCK_CASE_ID
assert response.json()[1]["case_id"] == MOCK_CASE_ID_2
Здесь я создал два тестовых примера, создал соответствующие приспособления или макеты, я думаю, для клиента fastapi и Case_service, который выполняет выборку (функция get_all_cases). Я понимаю, что эти две строки "издеваются" над извлечением дел из базы данных:

Код: Выделить всё

mock_cases = [MOCK_DB_CASE, MOCK_DB_CASE_2]
mock_case_service.get_all_cases = AsyncMock(return_value=[CaseInDB.model_validate(case) for case in mock_cases])
Но как тестовый клиент запускает эту конечную точку и извлекает два ложных случая вместо обращения к базе данных? Я понимаю, что это объект TestClient, но откуда он знает, что нужно получить эти ложные случаи?
А вот реальный код, который попадает в базу данных:

Код: Выделить всё

class CaseRepository(BaseRepository[Case, CaseCreate, CaseUpdate]):
def __init__(self):
super().__init__(Case)

async def get_all(self, db: Session, agent_id: str) -> List[Case]:
return (
db.query(self.model)
.filter(self.model.agent_id == agent_id)
.order_by(desc(self.model.case_id))  # Order by case_id in descending order
.all()
)
А это модульный тест для проверки этой части:

Код: Выделить всё

class TestCaseService:

@pytest.mark.asyncio
async def test_get_all_cases(self, case_service, mock_repository, mock_db):

mock_cases = [MOCK_DB_CASE, MOCK_DB_CASE_2]
mock_repository.get_all = AsyncMock(return_value=mock_cases)

result = await case_service.get_all_cases(mock_db, MOCK_AGENT_ID)

assert len(result) == 2
assert isinstance(result[0], CaseInDB)
assert isinstance(result[1], CaseInDB)
assert result[0].case_id == MOCK_CASE_ID
assert result[1].case_id == MOCK_CASE_ID_2
mock_repository.get_all.assert_awaited_once_with(mock_db, MOCK_AGENT_ID)
Опять же, я понимаю, что первые две строки «имитируют» получение двух макетных случаев с использованием этой функции get_all. Но теперь мы используем фикстуру case_service, чтобы получить все случаи из макета_db. Как он возвращает эти два ложных случая, а не реальные случаи в базе данных SQL?

Подробнее здесь: https://stackoverflow.com/questions/793 ... t-unittest
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Python»