| name | test-writer |
| description | Guides test creation for Polibase following strict testing standards. Activates when writing tests or creating test files. Enforces external service mocking (no real API calls), async/await patterns, test independence, and proper use of pytest-asyncio to prevent CI failures and API costs. |
Test Writer
Purpose
Guide test creation following Polibase testing standards with proper mocking, async/await patterns, and independence from external services.
When to Activate
This skill activates automatically when:
- Writing new tests
- Creating test files in
tests/directory - User mentions "test", "pytest", or "testing"
- Reviewing existing test code
⥠TDD Workflow (Test-First Development)
ALWAYS write tests BEFORE implementation!
Red-Green-Refactor Cycle
ðī Red: Write a failing test
# Write test first - it will fail (no implementation yet) @pytest.mark.asyncio async def test_create_politician_saves_to_repository(): mock_repo = AsyncMock(spec=IPoliticianRepository) mock_repo.create.return_value = Politician(id=1, name="åąąį°åĪŠé") usecase = CreatePoliticianUseCase(mock_repo) result = await usecase.execute(CreatePoliticianInputDTO(name="åąąį°åĪŠé")) mock_repo.create.assert_awaited_once()ðĒ Green: Write minimal code to pass
# Now implement just enough to make test pass class CreatePoliticianUseCase: async def execute(self, input_dto): politician = Politician(name=input_dto.name) await self.repository.create(politician)âŧïļ Refactor: Improve code while keeping tests green
# Refactor with confidence - tests verify behavior class CreatePoliticianUseCase: async def execute(self, input_dto): # Add validation if not input_dto.name: raise ValueError("Name required") # Extract to method politician = self._create_entity(input_dto) return await self.repository.create(politician)
TDD Benefits
- â Forces you to think about API design before implementation
- â Tests serve as documentation
- â Refactoring is safe (tests catch regressions)
- â Code is naturally testable (designed for testing)
Remember: If you write implementation first, you're not doing TDD!
ðŦ CRITICAL: Never Call External Services
ABSOLUTELY FORBIDDEN in tests:
- â Real API calls to Google Gemini or any LLM
- â Actual HTTP requests to external websites
- â Real database connections (except integration tests)
- â File system operations outside temp directories
- â Network connections of any kind
Why?
- Tests must run in CI/CD without API keys
- Tests must be fast (< 1 second per test)
- Tests must be deterministic (same result every time)
- Tests must not incur API costs
Quick Checklist
Before committing tests:
- No External Calls: All external services mocked
- Fast Execution: Each test runs in < 1 second
- Isolated: Tests don't depend on each other
- Deterministic: Same result every time
- Clear Names: Test name describes what it tests
- Arrange-Act-Assert: Clear test structure
- Async Properly: Uses
@pytest.mark.asyncioandAsyncMock - Mock Verification: Asserts mock calls when relevant
- Type Hints: Complete type annotations
Test Structure
tests/
âââ unit/ # Fast, isolated tests
â âââ domain/ # Domain entities and services
â âââ application/ # Use cases (with mocks)
â âââ infrastructure/ # External services (with mocks)
âââ integration/ # Tests with real database
âââ evaluation/ # LLM evaluation (manual only, not in CI)
âââ conftest.py # Shared fixtures
Core Testing Patterns
1. Mocking External Services
Always use AsyncMock with spec= parameter:
from unittest.mock import AsyncMock
@pytest.fixture
def mock_llm_service():
# ALWAYS use spec= to catch typos and wrong method calls
mock = AsyncMock(spec=ILLMService)
mock.generate_text.return_value = "Mocked response"
return mock
â ïļ Why spec= is CRITICAL:
# â WITHOUT spec= - typos go undetected
mock = AsyncMock()
await mock.genrate_text("prompt") # Typo! Test still passes!
# â
WITH spec= - typos caught immediately
mock = AsyncMock(spec=ILLMService)
await mock.genrate_text("prompt") # AttributeError!
Use AsyncMock for async methods, never MagicMock:
# â WRONG - MagicMock for async function
mock_repo = MagicMock(spec=IPoliticianRepository)
result = await mock_repo.create(politician) # Error!
# â
CORRECT - AsyncMock for async function
mock_repo = AsyncMock(spec=IPoliticianRepository)
result = await mock_repo.create(politician) # Works!
2. Async Tests
Use pytest-asyncio:
@pytest.mark.asyncio
async def test_async_function(mock_repo):
result = await usecase.execute(input_dto)
assert result.success
3. Test Independence
Each test is self-contained:
def test_create_politician(mock_repo):
# Setup mock
mock_repo.save.return_value = Politician(id=1, name="Test")
# Execute
result = usecase.execute(input_dto)
# Assert
assert result.success
Templates
Use templates in templates/ directory for:
- Domain service tests
- Use case tests with mocks
- Repository integration tests
- External service tests with mocks
Detailed Reference
For comprehensive testing patterns, mocking strategies, and best practices, see reference.md.
Examples
See examples.md for concrete test examples at each layer.
Running Tests
# Run all tests
docker compose -f docker/docker-compose.yml [-f docker/docker-compose.override.yml] exec polibase uv run pytest
# Run specific test file
docker compose -f docker/docker-compose.yml [-f docker/docker-compose.override.yml] exec polibase uv run pytest tests/unit/domain/test_speaker_domain_service.py
# Run with coverage
docker compose -f docker/docker-compose.yml [-f docker/docker-compose.override.yml] exec polibase uv run pytest --cov=src
# Run only unit tests
docker compose -f docker/docker-compose.yml [-f docker/docker-compose.override.yml] exec polibase uv run pytest tests/unit/
Common Anti-Patterns
- â Real API Calls: Most common mistake!
- â Testing Implementation Details: Test public interfaces
- â Test Dependencies: Each test must be independent
- â Missing Async/Await: Forget
@pytest.mark.asyncio - â No Mock Verification: Don't check if mocks were called
See reference.md for detailed explanations and fixes.