When this skill is activated, always start your first response with the 🧢 emoji.
Test Strategy
A testing strategy answers three questions: what to test, at what level, and how much. Without a strategy, teams end up with either too many slow, brittle e2e tests or too few tests overall - both are expensive. This skill gives the judgment to design a test suite that provides high confidence, fast feedback, and low maintenance cost.
When to use this skill
Trigger this skill when the user:
- Asks which type of test to write for a given scenario
- Wants to design a testing strategy for a new service or feature
- Needs to decide between unit, integration, and e2e tests
- Asks about test coverage targets or metrics
- Wants to implement contract testing between services
- Is dealing with flaky tests and needs a remediation plan
- Asks about TDD or BDD workflow
Do NOT trigger this skill for:
- Writing the actual test code syntax for a specific framework (defer to framework docs)
- Performance testing or load testing strategy (separate domain)
Key principles
- Test behavior, not implementation - Tests should survive refactoring. If
moving logic between private methods breaks your tests, the tests are testing the wrong thing. Test public contracts and observable outcomes.
- The Testing Trophy over the pyramid - The classic pyramid (many unit,
fewer integration, few e2e) was coined before modern tooling. The Trophy (Kent C. Dodds) weights integration tests most heavily: static analysis at the base, unit tests for isolated logic, integration tests for the bulk of coverage, and a few e2e tests for critical paths.
- Fast feedback loops - A test suite that takes 30 minutes to run is a
test suite that doesn't get run. Design for speed: unit tests in milliseconds, integration tests in seconds, e2e tests reserved for CI only.
- Test at the right level - The cost of a test rises as you move up the
stack (slower, more brittle, harder to debug). Test each concern at the lowest level that meaningfully exercises it.
- Flaky tests are worse than no tests - A test that sometimes fails trains
the team to ignore failures. A flaky test in CI delays every deploy. Fix or delete flaky tests immediately; never tolerate them.
Core concepts
Test types taxonomy
| Type | What it tests | Speed | Cost | Use for | |---|---|---|---|---| | Static | Type errors, lint violations | Instant | Near-zero | Type safety, obvious mistakes | | Unit | Single function/class in isolation | < 10ms | Low | Pure logic, edge cases, algorithms | | Integration | Multiple modules together with real dependencies | 100ms-2s | Medium | Service layer, DB queries, API handlers | | E2E | Full user journey through deployed stack | 5-60s | High | Critical user paths, smoke tests | | Contract | API contract between producer and consumer | Seconds | Medium | Microservice boundaries |
The Testing Trophy
/\
/e2e\ - Few: critical flows only
/------\
/ integ \ - Most: service + DB + API
/------------\
/ unit \ - Some: pure logic and edge cases
/----------------\
/ static \ - Always: types, lint, format
/--------------------\
The key insight is that integration tests give the best ROI for most application code: they test real behavior through real dependencies without the brittleness of e2e tests.
Test doubles
Use the minimum isolation necessary for the test's purpose:
| Double | When to use | Risk | |---|---|---| | Stub | Replace slow/unavailable dependency, return canned data | Low - no behavior coupling | | Mock | Verify a side effect was triggered (email sent, event published) | Medium - couples to call signature | | Spy | Observe calls without replacing behavior | Medium - couples to call count/args | | Fake | Replace infrastructure with working in-memory version | Low - tests real behavior patterns |
Prefer fakes for infrastructure (in-memory DB, in-memory queue). Mocks should be reserved for side effects you cannot otherwise observe.
Coverage metrics
| Metric | What it measures | When to use | |---|---|---| | Line coverage | % of lines executed | Baseline floor, not a target | | Branch coverage | % of conditional paths taken | Better for logic-heavy code | | Mutation coverage | % of introduced bugs caught by tests | Gold standard for test quality |
Line coverage above ~80% has diminishing returns and creates perverse incentives. Mutation coverage reveals whether tests actually assert meaningful things.
Common tasks
Choose the right test type - decision matrix
When deciding what level to test something at, apply this logic:
Is this pure logic with no external dependencies?
YES → Unit test
NO → Does it require a real DB / HTTP call / file system?
YES → Integration test (use real infrastructure or a fast fake)
NO → Does it span multiple services or require a browser?
YES → E2E test (sparingly)
NO → Integration test
Additional rules:
- Cross-service API boundaries → Contract test (Pact or similar)
- Complex UI interaction that cannot be tested at component level → E2E
- Algorithm with many edge cases → Unit test per edge case + one integration
Design a test suite for a new service
Structure the test suite before writing the first line of code:
- Map the test surface - Identify all external I/O: databases, queues,
HTTP clients, file system. These are the integration seams.
- Choose infrastructure strategy - Real DB with test containers, in-memory
fake, or Docker Compose. Prefer real DBs for schema-heavy services.
- Define the testing trophy for your context - Decide the ratio before
you write tests. A typical distribution: 60% integration, 30% unit, 10% e2e.
- Set up test data factories - Centralize how test objects are created.
Factories prevent fragile fixtures and make tests self-documenting.
- Wire CI from day one - Tests that only run locally drift. Run unit +
integration in every PR, e2e in pre-merge or nightly.
Write effective unit tests - patterns
Unit tests work best for:
- Pure functions (same input always gives same output)
- Complex conditional logic with many branches
- Data transformations and parsing
- Domain model invariants
Arrange-Act-Assert structure:
test('applies 10% discount for orders over $100', () => {
// Arrange
const order = buildOrder({ subtotal: 120 });
// Act
const discounted = applyLoyaltyDiscount(order);
// Assert
expect(discounted.total).toBe(108);
});
Parameterize boundary conditions:
test.each([
[99, 0], // just below threshold - no discount
[100, 10], // exactly at threshold
[200, 20], // above threshold
])('order of $%i gets $%i discount', (subtotal, expectedDiscount) => {
const order = buildOrder({ subtotal });
expect(applyLoyaltyDiscount(order).discount).toBe(expectedDiscount);
});
See references/test-patterns.md for more patterns.
Write integration tests - database and API
For database integration tests:
// Use real DB, roll back after each test
beforeEach(() => db.beginTransaction());
afterEach(() => db.rollbackTransaction());
test('saves user and returns with id', async () => {
const user = await userRepo.create({ name: 'Alice', email: 'alice@test.com' });
expect(user.id).toBeDefined();
const found = await userRepo.findById(user.id);
expect(found.name).toBe('Alice');
});
For HTTP API integration tests, test the full request cycle:
test('POST /orders returns 201 with order id', async () => {
const response = await request(app)
.post('/orders')
.send({ items: [{ productId: 'p1', qty: 2 }] });
expect(response.status).toBe(201);
expect(response.body.orde