| name | testing |
| description | Test development with pytest, fixtures, and integration testing. Use for writing tests, test patterns, coverage, parametrization, and debugging test failures. |
Testing Skill
Philosophy: See .claude/rules/integration-testing.md (test real services, mock only external network APIs).
Running Tests
| Test Type | Location | Command |
|---|---|---|
| Backend | Docker | docker compose exec backend pytest tests/ |
| Frontend | Docker | docker compose exec frontend pnpm test |
| E2E | Host | cd playwright && pnpm test |
# All quality gates
just check
# Backend with coverage
docker compose exec backend pytest tests/ --cov=app --cov-report=term-missing
# Specific test
docker compose exec backend pytest tests/test_signal_chains.py -v -k "test_create"
# Stop on first failure
docker compose exec backend pytest -x
# E2E specific test
cd playwright && pnpm test --grep "signal chain"
Test Structure
backend/tests/
├── conftest.py # Shared fixtures
├── test_*.py # Integration tests (real DB)
playwright/tests/
├── *.spec.ts # E2E tests (real backend)
├── fixtures/ # Test helpers
└── global-setup.ts # DB seeding
Fixtures (conftest.py)
@pytest.fixture
async def db_session(async_engine) -> AsyncSession:
"""Real database with transaction rollback."""
async with async_engine.begin() as conn:
session = AsyncSession(bind=conn)
yield session
await conn.rollback()
@pytest.fixture
async def authenticated_user(db_session: AsyncSession) -> dict:
"""Create real test user."""
user = User(username="test-user", tone3000_id="test-123")
db_session.add(user)
await db_session.commit()
token = create_access_token(user.id)
return {"user": user, "token": token}
Test Patterns
Integration Test (Preferred)
@pytest.mark.asyncio
async def test_create_signal_chain(async_client: AsyncClient, authenticated_user: dict):
response = await async_client.post(
"/api/v1/signal-chains",
json={"name": "Test Chain", "platform": "nam"},
headers={"Authorization": f"Bearer {authenticated_user['token']}"},
)
assert response.status_code == 201
assert response.json()["id"] is not None # Real DB ID
Unit Test (Pure Logic Only)
def test_signal_chain_validates_name():
with pytest.raises(ValueError, match="Name cannot be empty"):
SignalChain(name="", platform="nam")
E2E Test (No Mocking)
test('creates signal chain', async ({ page }) => {
await page.goto('/builder');
await page.fill('[name="chain-name"]', 'My Chain');
await page.click('button:has-text("Save")');
// Verify in real database
const response = await page.request.get('/api/v1/signal-chains');
const chains = await response.json();
expect(chains.signal_chains.some(c => c.name === 'My Chain')).toBe(true);
});
Parametrized Test
@pytest.mark.parametrize("platform,valid", [
("nam", True),
("aida_x", True),
("invalid", False),
])
def test_platform_validation(platform: str, valid: bool):
if valid:
chain = SignalChain(name="Test", platform=platform)
assert chain.platform == platform
else:
with pytest.raises(ValueError):
SignalChain(name="Test", platform=platform)
Mocking External APIs
async def test_t3k_sync_handles_error():
"""Mock EXTERNAL API only."""
with patch("app.services.t3k_client.fetch_tones") as mock:
mock.side_effect = ExternalAPIError("T3K down")
result = await sync_service.sync_user_tones(user_id)
assert result.status == "failed"
Test Data
Seed Data (from seed.sql)
| User | Username | UUID | Purpose |
|---|---|---|---|
| Primary | e2e-user |
e2e00000-...-000000000001 |
Main test user |
| Other | e2e-other-user |
e2e00000-...-000000000002 |
Permission tests |
Test Data Prefix
Use E2E-Test- prefix for cleanup:
const chainName = `E2E-Test-My-Chain`;
Cleanup
- Backend: Transaction rollback (automatic)
- E2E: Delete
E2E-Test-*entities inafterEach
Coverage
Target: 80%+
docker compose exec backend pytest tests/ --cov=app --cov-fail-under=80