testing
Testing strategies and best practices — TDD workflow, test pyramid, unit/integration/e2e patterns, and writing maintainable tests across languages.
SKILL.mdskills/testing/SKILL.md
GitHubtesting
Testing strategies and best practices — TDD workflow, test pyramid, unit/integration/e2e patterns, and writing maintainable tests across languages.
Instructions
Apply these testing practices when writing or reviewing tests:
Guidelines
Type Safety in Tests
- Test helpers, factories, and fixtures must be fully typed — no
anyin test setup - Use typed mocks: prefer
jest.mocked<T>()/vi.mocked<T>()over casting toany - Assert on precise types where possible to catch type regressions alongside logic regressions
Test Quality Rules
- One logical behaviour per test — tests that assert multiple unrelated things are hard to diagnose when they fail
- No magic values in assertions — extract expected values into named constants with explanatory names
- Tests are first-class code: apply the same quality rules (naming, size limits, DRY) to test files as to production code
- Deterministic only: no
Date.now(),Math.random(), network calls, or filesystem side-effects in unit tests — mock or inject them
Coverage Requirements
| Layer | Minimum coverage |
|---|---|
| Business logic (services) | ≥ 80% |
| Utility functions | ≥ 90% |
| Controllers / route handlers | ≥ 70% |
| Generated / boilerplate code | exempt |
CI Requirements
- Tests must run in CI on every push and pull request
- Failing tests must block merge — no bypassing the test gate
- Unit + integration test suite must complete in < 5 minutes; E2E runs in a separate, optional stage
The Test Pyramid
Follow the classic test pyramid for a balanced, fast, and reliable test suite:
/\
/e2e\ ← few, slow, high confidence
/------\
/integr-\ ← moderate, medium speed
/----------\
/ unit tests \ ← many, fast, isolated
/______________\
- Unit tests (70%): test a single function/class in isolation; mock all dependencies
- Integration tests (20%): test interactions between components (DB, services, APIs)
- E2E tests (10%): test full user flows through the real system; use sparingly
TDD Workflow (Red–Green–Refactor)
- Red – write a failing test that describes the desired behaviour
- Green – write the minimal code to make the test pass
- Refactor – clean up the code without breaking the tests
Writing Good Tests
Structure with AAA (Arrange–Act–Assert):
it('should return the total price including tax', () => {
// Arrange
const cart = new Cart([{ price: 100 }, { price: 50 }]);
const taxRate = 0.2;
// Act
const total = cart.totalWithTax(taxRate);
// Assert
expect(total).toBe(180);
});
Test naming conventions:
should <do something> when <condition>
given <context>, when <action>, then <outcome>
Principles:
- One assertion per test (or one logical concept)
- Deterministic — no reliance on system time, random numbers, or external services
- Isolated — tests must not depend on execution order
- Fast — unit tests should complete in milliseconds
- Readable — test code is documentation; prioritise clarity
Mocking & Stubbing
- Mock at the boundary: mock external services, databases, and APIs — not internal functions
- Prefer dependency injection over module mocking where possible
- Verify mock calls only when the interaction itself is the behaviour under test
// Prefer: inject a mock via constructor/parameter
const service = new OrderService({ emailClient: mockEmailClient });
// Avoid: mock an internal module unless necessary
jest.mock('../internal/utils');
Coverage Guidelines
- Aim for ≥ 80% line/branch coverage on business logic
- Do NOT write tests just to hit a coverage number — meaningless tests are worse than no tests
- Use coverage reports to find untested edge cases, not as a target metric
- Exclude generated code, config files, and boilerplate from coverage reports
Integration Testing Patterns
- Use testcontainers to spin up real DB/Redis/etc. in CI
- Use database transactions that roll back after each test for isolation
- Use contract testing (e.g., Pact) for service-to-service boundaries
E2E Testing Guidelines
- Write E2E tests for the most critical user journeys only
- Use stable selectors (
data-testid) over CSS classes or text - Keep E2E tests independent and idempotent (seed + clean up their own data)
Examples
Node.js (Vitest/Jest):
describe('calculateDiscount', () => {
it('should apply 10% discount for orders over $100', () => {
expect(calculateDiscount(150)).toBe(135);
});
it('should return full price for orders under $100', () => {
expect(calculateDiscount(80)).toBe(80);
});
it('should throw for negative amounts', () => {
expect(() => calculateDiscount(-10)).toThrow('Amount must be positive');
});
});
Python (pytest):
def test_discount_applied_for_large_orders():
assert calculate_discount(150) == 135
def test_no_discount_for_small_orders():
assert calculate_discount(80) == 80
def test_raises_for_negative_amount():
with pytest.raises(ValueError, match="Amount must be positive"):
calculate_discount(-10)