7.4 KiB
trigger, description
| trigger | description |
|---|---|
| model_decision | When writing tests, organizing test files, implementing test doubles, or setting up testing infrastructure |
Testing Strategy
Test Pyramid
Unit Tests (70% of tests):
- What: Test domain logic in isolation with mocked dependencies
- Speed: Fast (<100ms per test)
- Scope: Single function, class, or module
- Dependencies: All external dependencies mocked (repositories, APIs, time, random)
- Coverage Goal: >85% of domain logic
Integration Tests (20% of tests):
- What: Test adapters against real infrastructure
- Speed: Medium (100ms-5s per test)
- Scope: Component interaction with infrastructure (database, cache, message queue)
- Dependencies: Real infrastructure via Testcontainers
- Coverage Goal: All adapter implementations, critical integration points
End-to-End Tests (10% of tests):
- What: Test complete user journeys through all layers
- Speed: Slow (5s-30s per test)
- Scope: Full system from HTTP request to database and back
- Dependencies: Entire system running (or close approximation)
- Coverage Goal: Happy paths, critical business flows
Test-Driven Development (TDD)
Red-Green-Refactor Cycle:
- Red: Write a failing test for next bit of functionality
- Green: Write minimal code to make test pass
- Refactor: Clean up code while keeping tests green
- Repeat: Next test
Benefits:
- Tests written first ensure testable design
- Comprehensive test coverage (code without test doesn't exist)
- Faster development (catch bugs immediately, not in QA)
- Better design (forces thinking about interfaces before implementation)
Test Doubles Strategy
Unit Tests: Use mocks/stubs for all driven ports
- Mock repositories return pre-defined data
- Mock external APIs return successful responses
- Mock time/random for deterministic tests
- Control test environment completely
Integration Tests: Use real infrastructure
- Testcontainers spins up PostgreSQL, Redis, message queues
- Firebase emulator spins up Firebase Authentication, Cloud Firestore, Realtime Database, Cloud Storage for Firebase, Firebase Hosting, Cloud Functions, Pub/Sub, and Firebase Extensions
- Test actual database queries, connection handling, transactions
- Verify adapter implementations work with real services
Best Practice:
- Generate at least 2 implementations per driven port:
- Production adapter (PostgreSQL, GCP GCS, etc.)
- Test adapter (in-memory, fake implementation)
Test Organization
Universal Rule: Co-locate implementation tests; Separate system tests.
1. Unit & Integration Tests (Co-located)
- Rule: Place tests next to the file they test.
- Why: Keeps tests visible, encourages maintenance, and supports refactoring (moving a file moves its tests).
- Naming Convention Example:
- TS/JS:
*.spec.ts(Unit),*.integration.spec.ts(Integration) - Go:
*_test.go(Unit),*_integration_test.go(Integration) - Dart/Flutter:
*_test.dart(Unit),*_integration_test.dart(Integration) — tests live intest/mirroringlib/layout (Flutter's default convention, discovered byflutter test) - Python:
test_*.py(Unit),test_*_integration.py(Integration) - Java:
*Test.java(Unit),*IT.java(Integration) - Rust:
#[cfg(test)] mod testsinline in each.rsfile (Unit),tests/directory at crate root (Integration)
You must strictly follow the convention for the target language. Do not mix
testandspecsuffixes in the same application context. Language-specific overrides: Some ecosystems have different test location conventions. When a language-specific project structure file exists (e.g.,project-structure-flutter-mobile.md,project-structure-rust-cargo.md), its test location rules take precedence over the co-location default. Seearchitectural-pattern.md§ Test co-location for the authoritative rule. Rust exception: Rust unit tests are inline (#[cfg(test)] mod testsat the bottom of each.rsfile), not separate files. This is the official Rust convention — it enables testing private functions and is howcargo testdiscovers unit tests. Integration tests go intests/at the crate root, compiled as separate crates. Seerust-idioms-and-patterns.mdfor details. - TS/JS:
2. End-to-End Tests (Separate)
- Rule: Place in a dedicated
e2e/folder- Single app:
e2e/at project root - Monorepo:
apps/e2e/subdivided by test scope:apps/e2e/api/for full API flow E2E tests (HTTP → Database)apps/e2e/ui/for full-stack E2E tests (Browser → Backend → Database)
Path adapts to project type per
project-structure.md§ Adapting for Different Project Types. - Single app:
- Naming: Follow
{feature}-{ui/api}.e2e.test.{ext}(Universal - configure test runner to match this pattern**/*.e2e.test.*)- Example:
user-registration-api.e2e.test.ts# Full API flow: HTTP → DBuser-registration-ui.e2e.test.ts# Full-stack: Browser → Backend → DB
- Example:
Using Playwright MCP for UI E2E Tests:
When running E2E tests interactively (during development or verification), use Playwright MCP:
# Navigate to the page
mcp_playwright_browser_navigate(url="http://localhost:5173/login")
# Capture accessible state (better than screenshot for assertions)
mcp_playwright_browser_snapshot()
# Interact with elements by ref from snapshot
mcp_playwright_browser_type(ref="<ref>", text="[email protected]")
mcp_playwright_browser_click(ref="<ref>")
# Wait for results
mcp_playwright_browser_wait_for(text="Welcome")
# Capture snapshot for walkthrough documentation (captures full accessibility tree)
mcp_playwright_browser_snapshot(filename="login-success.md")
E2E Test Requirements:
- Capture a snapshot (
mcp_playwright_browser_snapshot) at each major step - Save snapshots to walkthrough as proof of functionality
- If visual proof is needed, use
browser_subagentwithRecordingNameto produce a recorded video artifact - Test happy path AND at least one error path
- Clean up test data after test (or use unique identifiers)
Key Principles:
- Unit/Integration tests: Co-located with implementation
- E2E tests: Separate directory (crosses boundaries)
- Test doubles: Co-located with interface (storage_mock.go, taskAPI.mock.ts)
- Pattern consistency: All features follow same structure
Test Quality Standards
AAA Pattern (Arrange-Act-Assert):
// Arrange: Set up test data and mocks
const user = { id: '123', email: '[email protected]' };
const mockRepo = createMockRepository();
// Act: Execute the code under test
const result = await userService.createUser(user);
// Assert: Verify expected outcome
expect(result.id).toBe('123');
expect(mockRepo.save).toHaveBeenCalledWith(user);
Test Naming:
- Descriptive:
should [expected behavior] when [condition] - Examples:
should return 404 when user not foundshould hash password before saving to databaseshould reject email with invalid format
Coverage Requirements:
- Unit tests: >85% code coverage
- Integration tests: All adapter implementations
- E2E tests: Critical user journeys
Related Principles
- Architectural Patterns — Testability-First Design @architectural-pattern.md
- Error Handling Principles @error-handling-principles.md
- Project Structure @project-structure.md