| name | test-strategy |
| description | Use when choosing a testing model for a new project, auditing a test suite that is slow or provides low confidence, setting coverage targets, or writing a QA test plan for a release. |
Test Strategy
Design a complete testing strategy for any project: choose the right model, set meaningful coverage targets, shift quality left, and plan non-functional testing.
When to Activate
- Starting a new project and deciding on a testing approach
- Auditing an existing test suite that is slow or provides low confidence
- Writing a QA plan or test plan document
- Deciding how to balance unit vs integration vs E2E tests for a feature
- Setting coverage targets for a team or project
- Planning non-functional testing (load, security, accessibility)
Testing Models
Choosing the right testing model is the first decision. Each reflects a different philosophy about where confidence comes from.
The Pyramid (Classic)
/\
/E2E\ few (slow, fragile)
/------\
/ Integ \ some
/------------\
/ Unit \ many (fast, reliable)
/-----------------\
- Best for: well-defined layers, strong service boundaries, experienced team
- Risk: integration tests are often underdone; false confidence from high unit coverage
The Trophy (Kent C. Dodds)
/\
/E2E\ few
/------\
/ \
/ Integra- \ most ← emphasis here
/ tion \
/---------\
/ Unit \ some
/ (static) \ type checking, linting
- Best for: React/frontend apps, services where user behavior drives quality
- The "integration" layer tests realistic slices (full request/response, not mocked)
The Honeycomb (Spotify / Microservices)
- Emphasized: service integration tests (call your API, hit a real DB)
- De-emphasized: pure unit tests (too many mocks = low confidence)
- Best for: microservices, event-driven systems
Decision Table
| Context | Recommended Model | Reason |
|---|
| Monolith, complex business logic | Pyramid | Units test business rules cheaply |
| Frontend-heavy application | Trophy | Integration tests reflect user behavior |
| Microservices (many small services) | Honeycomb | Service integration > unit isolation |
| Data pipeline | Custom (mostly integration) | Units are trivial; real data matters |
Coverage Target Setting
What Coverage Measures
| Metric | What It Measures | How to Get It |
|---|
| Line coverage | Were these lines executed? | --cov, --coverage, go test -cover |
| Branch coverage | Were all if/else paths taken? | --branch flag |
| Mutation coverage | Do tests catch logic mutations? | mutmut (Python), stryker (TS), go-mutesting |
Realistic Targets
| Codebase Type | Line Coverage Target | Notes |
|---|
| New greenfield project | 80%+ | Enforce from day 1 |
| Adding tests to legacy | Raise by 5% per sprint | Ratchet: never let it drop |
| Critical path (payments, auth) | 95%+ | Include branch coverage |
| Generated code, migrations, config loaders | Exclude from measurement | Noisy, not meaningful |
Rule: coverage is a floor, not a goal. 60% with excellent integration tests > 100% with trivial mocks.
Enforcing Coverage in CI
[tool.pytest.ini_options]
addopts = "--cov=src --cov-fail-under=80 --cov-branch"
[tool.coverage.report]
omit = ["src/migrations/*", "src/generated/*", "**/config_loader.py"]
{
"jest": {
"coverageThreshold": {
"global": {
"lines": 80,
"branches": 70
}
},
"coveragePathIgnorePatterns": ["/generated/", "/migrations/"]
}
}
Shift-Left Testing
Shift-left = catch defects earlier in the development cycle (before code review, before CI).
| Technique | When It Runs | What It Catches |
|---|
| Type checking (mypy, tsc, go vet) | IDE + pre-commit | Type errors, wrong function signatures |
| Linting (ruff, eslint, staticcheck) | IDE + pre-commit | Style, common bugs, dead code |
| Pre-commit hooks | On git commit | Both above, secret scanning |
| Contract tests | CI on PR | API contract violations between services |
| Property-based tests | CI | Edge cases the developer didn't think of |
Pre-Commit Configuration
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
Contract Testing (Pact)
Contract tests verify that two services agree on the shape of requests and responses without needing both services running simultaneously.
const interaction = {
state: "user 123 exists",
uponReceiving: "a request for user 123",
withRequest: { method: "GET", path: "/users/123" },
willRespondWith: {
status: 200,
body: { id: 123, name: like("Alice") },
},
};
Property-Based Testing
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sort_is_idempotent(lst):
assert sorted(sorted(lst)) == sorted(lst)
Non-Functional Test Types
| Type | What It Tests | Tools | When to Run |
|---|
| Load testing | Behavior under expected traffic | k6, Locust, JMeter | Pre-launch, nightly |
| Stress testing | Behavior beyond capacity | k6, Gatling | Before scaling decisions |
| Soak testing | Behavior over extended time (memory leaks) | k6, Locust | Weekly |
| Spike testing | Sudden traffic burst handling | k6 | Before big events |
| Security testing | Vulnerability scanning | OWASP ZAP, Snyk, pip-audit | Every CI run (SAST), nightly (DAST) |
| Accessibility (a11y) | WCAG compliance | axe-core, Playwright + axe | Every PR for UI changes |
| Visual regression | Unintended UI changes | Playwright screenshots, Percy | Every PR for UI changes |
k6 Load Test Example
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "1m", target: 50 },
{ duration: "3m", target: 50 },
{ duration: "1m", target: 0 },
],
thresholds: {
http_req_duration: ["p(95)<500"],
http_req_failed: ["rate<0.01"],
},
};
export default function () {
const res = http.get("https://api.example.com/health");
check(res, { "status is 200": (r) => r.status === 200 });
sleep(1);
}
Accessibility Testing with Playwright
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("homepage has no WCAG violations", async ({ page }) => {
await page.goto("/");
const results = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa"])
.analyze();
expect(results.violations).toEqual([]);
});
Test Plan Template
# Test Plan: [Feature / Release Name]
## Scope
What is being tested:
- [Feature 1]
- [Feature 2]
## Out of Scope
- [Explicitly excluded items]
## Test Environments
| Environment | URL | Data State |
|-------------|-----|-----------|
| Staging | ... | Anonymized copy of prod |
## Test Types and Owners
| Type | Owner | Tools | When |
|------|-------|-------|------|
| Unit | Dev | pytest/Jest/Go test | Every PR |
| Integration | Dev | Testcontainers | Every PR |
| E2E smoke | QA | Playwright | Post-deploy |
| Load | SRE | k6 | Pre-launch |
## Entry Criteria
- [ ] Feature code merged to main
- [ ] CI green
## Exit Criteria
- [ ] All P0/P1 test cases pass
- [ ] No open CRITICAL/HIGH bugs
- [ ] Coverage >= 80%
- [ ] Smoke tests pass on staging
## Risk Areas
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|-----------|
| ... | ... | ... | ... |
See also: unit-testing, integration-testing, solution-testing, performance-testing
Red Flags
- Applying the test pyramid without considering system architecture — the pyramid assumes cheap unit tests; for integration-heavy microservices or event-driven systems, the Honeycomb model often fits better
- Coverage percentage as the primary quality metric — 90% line coverage can coexist with zero behavior coverage if tests assert on implementation rather than outcomes; track branch coverage and mutation scores
- E2E tests for edge cases and error paths — edge cases should live in unit or integration tests; E2E tests should cover critical user journeys only, not every conditional branch
- Consumer-driven contract tests treated as optional — for service-to-service dependencies, a broken contract is a production outage; Pact catches this class of failure in CI before it ships
- Load and security tests planned for "after launch" — non-functional tests deferred post-launch are perpetually skipped; include them in the Definition of Done for every API feature
- No test plan before a major release — releases without a test plan have undefined risk; write a one-page plan listing scenarios, owners, and pass/fail criteria before any major release
- Shared mutable test state across the suite — a test that leaves the database dirty causes cascading failures in subsequent tests; treat test isolation as a first-class constraint
Checklist