testingreliabilityengineering

Engineering Reliability: A Testing & Quality Framework for Modern Web Platforms

Engineering Reliability: A Testing & Quality Framework for Modern Web Platforms

A practical framework covering every layer — from fast unit tests to full-stack E2E — and the quality gates that make merging safe.

1 The Testing Pyramid

The pyramid is a prioritisation tool, not a dogma. Its core message: write many fast tests at the bottom, few slow tests at the top. Every layer catches a different class of bug. Missing a layer means bugs escape to the layer above — where they cost more to find.

Unit Tests fast · isolated · no network · many · < 1s each Integration Tests real DB · transaction rollback · no mocks · < 30s Contract Tests API ↔ consumer shape · schema drift · < 5s Component Tests UI in isolation · no real API · < 10s E2E Tests full stack · < 10 min Manual Many Fewer Some Some Few Rare

The pyramid shape is a cost signal: lower layers run faster and cheaper. Push testing down as far as possible.

2 Seven Test Types — Deep Dive

Each type answers a different question. Using the wrong type costs time; skipping a type lets bugs through.

Unit Test < 1s

Tests a single function or class in complete isolation. No network, no database, no filesystem. External dependencies are replaced with fakes or mocks at the boundary. The fast feedback loop that makes TDD practical.

def test_hash_password_is_deterministic():
    # No DB, no IO — pure function test
    h1 = hash_password("secret")
    h2 = hash_password("secret")
    assert h1 == h2
    assert h1 != "secret"

Integration Test < 30s

Exercises real infrastructure — a real database, a real message queue. Each test runs inside a transaction that is rolled back at teardown, so tests remain isolated even though they touch real storage. No mocking of the layer being tested.

async def test_bootstrap_409_if_already_running(client, db):
    r1 = await client.post("/api/v1/ansible/bootstrap",
        json={"minion_id": "mac-01", "target_ip": "203.0.113.11"})
    assert r1.status_code == 200

    r2 = await client.post("/api/v1/ansible/bootstrap",
        json={"minion_id": "mac-01", "target_ip": "203.0.113.11"})
    assert r2.status_code == 409

Contract Test < 5s

The producer (API) and consumer (frontend) independently verify they agree on data shapes. The contract is explicit — a Pydantic schema on one side, a TypeScript interface on the other. A contract test fails if either side drifts. Most teams skip this layer entirely; they discover contract breaks when users see blank screens.

# Producer side — Pydantic schema IS the contract
class BootstrapResponse(BaseModel):
    node_id: uuid.UUID
    minion_id: str
    job_id: str
    bootstrap_status: str
    message: str
    salt_key_deleted: bool = False

// Consumer side TypeScript interface IS the contract
interface BootstrapResponse {
  node_id: string
  minion_id: string
  job_id: string
  bootstrap_status: string
  message: string
  // salt_key_deleted missing contract is already drifted
}

Component Test < 10s

Renders a single React component with a mocked API, verifying it handles loading, success, and error states correctly. Uses Vitest + Testing Library. Does not open a browser — runs in jsdom. Faster than E2E by an order of magnitude.

// BootstrapModal.test.tsx
it('shows error state when bootstrap API fails', async () => {
  server.use(
    rest.post('/api/v1/ansible/bootstrap', (req, res, ctx) =>
      res(ctx.status(409, 'Already bootstrapping')))
  )
  render(<BootstrapModal isOpen={true} />)
  await userEvent.click(screen.getByRole('button', { name: /start/i }))
  await screen.findByText(/already being bootstrapped/i)
})

End-to-End Test < 10 min total suite

A real browser, a real server, a real database. Playwright navigates the full user journey as a user would. Tests user behaviour, not implementation. Slow to run, high confidence, expensive to maintain. Should cover journeys, not every edge case — those belong lower in the pyramid.

test('BOOT-01 modal opens from fleet dashboard', async ({ page }) => {
  await page.click('button:has-text("+ Bootstrap Node")')
  await expect(page.locator('h2:has-text("Bootstrap Mac Mini")'))
    .toBeVisible()
})

Mutation Test minutes

A mutation testing tool (mutmut, Stryker) systematically breaks production code — flips a == to !=, removes a return statement, deletes a branch — then checks if your tests catch each change. The mutation score is the percentage of mutations your tests detect. A score below 70% means your tests are passing for the wrong reasons.

# Run mutmut on a specific module
mutmut run --paths-to-mutate fleet_platform/core/auth.py
mutmut results   # shows surviving mutants = test gaps

Property-Based Test < 30s

Hypothesis (Python) or fast-check (TypeScript) generates hundreds of random inputs and verifies that an invariant always holds. Finds edge cases that humans forget to write — empty strings, negative numbers, Unicode, max-length values. Write one property test to replace dozens of example-based tests.

from hypothesis import given, strategies as st

@given(st.text(min_size=1, max_size=128))
def test_validate_minion_id_never_crashes(s):
    # Invariant: validation never raises an exception — only True/False
    result = is_valid_minion_id(s)
    assert isinstance(result, bool)

3 The TDD Cycle

Test-Driven Development is a discipline, not a suggestion. The cycle is short — three phases, typically two to ten minutes per iteration. The discipline: never write production code without a failing test first.

🔴 Write failing test 🟢 Write minimal code to pass 🔵 Refactor (test stays green) Typical cycle time: 2–10 minutes per iteration

The constraint that makes TDD valuable: if you cannot write a test first, it means the requirement is not yet clear enough to implement. Clarify the requirement before writing code.

4 Development Workflow

Every feature follows the same sequence. The critical property: tests are written before implementation, and CI runs them automatically. Nothing can merge without passing through every gate.

Issue created Spec / AC written Tests defined Branch cut Failing tests written Implement code Tests green PR opened + test plan CI runs all checks Review incl. tests Merge to main Issue closes DoD met ✓

5 Contract Testing — The Critical Gap

Most teams test the API in isolation and test the frontend in isolation. Neither test verifies that the two sides agree on the shape of data flowing between them. Contract tests close this gap explicitly.

The risk: without contract tests, a backend developer can rename a field, the unit tests pass, the E2E tests pass (if they test for something else), and the frontend breaks silently in production. The user sees a blank screen or NaN.

Three contract failure modes

01

Field removed

API removes message field → frontend renders undefined in the status banner

02

Type changed

Field changes from string to number → frontend renders NaN where a human-readable label should appear

03

Structure changed

Nested object refactored → TypeError: cannot read property of undefined — crashes the component silently

How to implement contract tests

Producer side (Python/FastAPI): Pydantic schemas ARE the contract. The API endpoint is declared with response_model=BootstrapResponse. FastAPI validates every response against the schema at runtime. Add a unit test asserting the schema’s JSON representation matches the expected shape.

Consumer side (TypeScript): TypeScript interfaces are the contract. Run tsc --noEmit in CI — any code that accesses a field that no longer exists in the interface will fail the build.

Bridge: extract Pydantic JSON schemas (model.model_json_schema()), export them as JSON files, and write a script that compares them to the TypeScript interfaces. Run this script in CI. Alert on any field name or type drift.

# schema_drift_check.py — run in CI after tsc
import json
from fleet_platform.schemas.ansible import BootstrapResponse

schema = BootstrapResponse.model_json_schema()
with open("frontend/src/schemas/BootstrapResponse.json", "w") as f:
    json.dump(schema, f, indent=2)

# CI then runs: npx ts-json-schema-generator --validate
# Any field mismatch exits non-zero → merge blocked

6 Quality Gates — What Blocks Merge

CI is the last line of defence before code hits main. Every gate on this list must be green before a PR can merge. There are no exceptions — removing a gate “just this once” is how technical debt accumulates.

  • TypeScript builds with zero errors (tsc --noEmit)
  • Unit tests pass — 100% of them, zero allowed failures
  • Integration tests pass
  • E2E test count must not decrease from previous run
  • Coverage threshold maintained (target: ≥ 60% lines)
  • Contract schema drift check passes (zero drift)
  • No secrets detected in diff (pre-push hook via gitleaks or trufflehog)
  • PR description links to an issue and includes a test plan

The pre-push hook is not optional. Secret leaks are permanent. Once a key is committed to a public repo, it must be rotated even if you force-push — git history is replicated immediately. Catch secrets before they leave the machine.

7 Agile Quality

Quality is not a phase after development — it is woven into every sprint event.

Acceptance criteria first

Write acceptance criteria in the issue before assigning it. AC defines done. If you cannot write AC, the work is not ready for development.

Definition of Done

Code written + tests written + no new regressions + PR reviewed and approved. All four. A feature without tests is not done — it is a liability.

Test review is code review

Reviewers check test completeness, not just implementation. Ask: does this test actually catch the bug it claims to catch? Does it test behaviour or implementation? Would this test survive a refactor?

Sprint retrospective metrics

Review flakiness rate, coverage trend, and mutation score every sprint. Trends matter more than snapshots. A declining mutation score signals test quality degradation before coverage numbers show it.

8 Metrics to Track

MetricTargetHow to measure
Unit test coverage≥ 60%pytest-cov, —cov-fail-under=60
Mutation score≥ 70%mutmut — track surviving mutants per sprint
E2E flakiness rate< 2%Track retry count in CI logs over 4-week window
Contract drift0 fieldsSchema comparison script in CI — exit non-zero on drift
MTTR (mean time to recovery)< 1 sprintTrack time from incident detection to closed issue
Test suite duration< 10 minCI timing — fast suites get run; slow suites get skipped

Enjoyed this post?

Get the next one in your inbox — only when I ship something worth reading.

Newsletter form not configured.

Or follow on Substack for the newsletter.

Comments via GitHub Discussions

Comments not configured. Set GISCUS env vars to enable.