---
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

```yaml
# pytest example (pyproject.toml)
[tool.pytest.ini_options]
addopts = "--cov=src --cov-fail-under=80 --cov-branch"

[tool.coverage.report]
omit = ["src/migrations/*", "src/generated/*", "**/config_loader.py"]
```

```json
// Jest example (package.json)
{
  "jest": {
    "coverageThreshold": {
      "global": {
        "lines": 80,
        "branches": 70
      }
    },
    "coveragePathIgnorePatterns": ["/generated/", "/migrations/"]
  }
}
```

```go
// Go example (Makefile)
// go test ./... -coverprofile=coverage.out && go tool cover -func=coverage.out | grep total
```

## 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

```yaml
# .pre-commit-config.yaml
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.

```typescript
// Consumer side (TypeScript/Pact)
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

```python
# Python / Hypothesis
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

```javascript
// k6 load test
import http from "k6/http";
import { check, sleep } from "k6";

export const options = {
  stages: [
    { duration: "1m", target: 50 },   // ramp up
    { duration: "3m", target: 50 },   // hold
    { duration: "1m", target: 0 },    // ramp down
  ],
  thresholds: {
    http_req_duration: ["p(95)<500"], // 95th percentile under 500ms
    http_req_failed: ["rate<0.01"],   // error rate under 1%
  },
};

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

```typescript
// Playwright + axe-core
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

```markdown
# 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

- [ ] Testing model chosen (Pyramid/Trophy/Honeycomb) and matches team context
- [ ] Coverage targets defined per layer and enforced in CI
- [ ] Branch coverage measured for critical business logic
- [ ] Pre-commit hooks configured for linting, type-checking, secret scanning
- [ ] Non-functional test types identified (at least: load testing and security scanning)
- [ ] Test plan written for major releases
- [ ] Generated code and config loaders excluded from coverage measurement
- [ ] Test suite runs in under 10 minutes in CI (unit + integration; E2E separate)
- [ ] Contract tests in place for any service-to-service API dependencies
- [ ] Accessibility tests run on every PR touching UI components
