Aller au contenu principal
Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Software Engineering Craft

Engineering craft beyond tooling — design patterns, refactoring, code review, advanced testing strategy, and reading code you did not write.

Testing Strategy Studio

Pyramid (classic)

Étape 0 / 0
Speed 100ms
Step 0 / 0
Phase —
Scenario —
Mut. score —
Status Ready
Shape breakdown unit / integration / E2E
Property / mutation / contract —
Step explanation

Pick a scenario and press Play to walk the testing-strategy loop.

—
Pseudocode
 

Testing Strategy Beyond Unit Tests

Intermediate (3/5) ~3 hours Test pyramid vs honeycomb vs trophy Property-based testing (random + shrinking) Contract tests (consumer-driven) Mutation testing as a coverage oracle Snapshot tests and their failure modes Test readability as a first-class concern Prereqs: Testing & Debugging Fundamentals
Quick Reference

pyramid

No registry entry found for algorithm id "pyramid". If this is a curriculum-only studio, the complexity and quick-reference panel is intentionally omitted.

Unit tests verify a function. Integration tests verify the wiring. End-to-end tests verify the user-visible outcome. A healthy test strategy uses all three, but in different proportions than the classical pyramid suggests — and adds a fourth weapon (property-based) that finds bugs the others structurally miss.

Pyramids, Honeycombs, and Trophies

ShapeRecommended mixFailure it warns against
Pyramid70% unit / 20% integration / 10% E2EE2E-heavy suites that are slow and flaky
HoneycombFew unit / many integration / few E2EMock-heavy units that pass while integration fails
TrophyMany integration / some unit / few E2ERefactors that break units but not behaviour

The honest truth: there is no universal mix. Pick the shape that maximises confidence per minute of test runtime for your codebase. A library is unit-heavy; a CRUD service is integration-heavy; a CLI is often E2E-heavy. Measure runtime, measure regressions caught, then trade off.

Property-Based Testing

A unit test asserts one example. A property-based test asserts an invariant and lets a generator throw thousands of inputs at it. The test states what must always be true; the harness finds the input that falsifies it — and shrinks that input to the smallest reproducer.

property: sort(list) preserves length
       AND   sort(list) is monotonic non-decreasing
       AND   sort(sort(list)) == sort(list)

harness: 10,000 random lists of ints, drift against these invariants

A property a database sort might satisfy: sort(xs ++ ys) == sort(sort(xs) ++ sort(ys)) for every pair of lists. A property a hash map satisfies: get(put(m, k, v), k) == v for every map, key, and value. The harness tries tens of thousands of inputs; when one breaks the property, it shrinks to [] or [0, 0] — the minimal failing case.

Property-based tests catch off-by-one and edge-of-domain bugs that exampled tests miss because no one wrote an example for the input that broke. Use them for pure logic (parsers, sorts, state machines, transformations over algebraic structures).

Contract Testing (Consumer-Driven)

A microservice test that mocks its upstream/downstream dependencies can pass while real integration breaks the moment a producer ships. A contract test fixes this: each consumer writes its expectations against a shared contract; each producer verifies it satisfies every contract before deploying.

Consumer writes: "I call POST /users with {name, email}; I expect 201 with {id} or 409"
Producer verifies: it passes everyone's contracts before it can ship

This is the anti-pattern decoder for “we have tests but integration still breaks in prod”. Tools like Pact automate this; the engineering practice is older than the tools. The principle is: the consumer owns the contract, not the producer.

Mutation Testing

Coverage tells you which lines ran. Mutation testing tells you whether your tests fail when your code changes — a much truer measure.

The tool: take every line of production code, apply small mutations (+ → -, == → !=, if (x) → if (true)), and re-run the tests. Each mutation that survives is a hole — a code change that nothing catches.

production code:    if (i < list.length)
mutation:           if (i <= list.length)
tests pass?         yes → surviving mutant → coverage lied
                    no  → caught mutant → test would have caught the regression

Mutation testing is expensive. Use it as a periodic audit, not a per-commit gate: a single run on critical modules exposes which “covered” lines actually have a behaviour test watching them.

Snapshot Tests: Cheap and Fragile

A snapshot test captures the serialised output of a component (typically: rendered React tree, JSON API response, generated HTML) and re-asserts it on every run. Cheap to write, expensive to maintain, because every refactor updates the snapshot whether or not it changed behaviour.

Snapshot failure rules:

  • Snapshots must be reviewed in diffs. A blind --updateSnapshot is the failure mode; it converts the test into a no-op.
  • Snapshots must name what they are testing: renders signup form with validation errors, not renders correctly.
  • Snapshots are not assertions — they are change detectors. Their drift and not their existence is the value.

Use them sparingly for output stability proofs; prefer property-based tests for pure logic, prefer integration tests for behaviour.

Test Readability

A test file is more often read than written, just like production code. Three habits compound:

  • Name tests by behaviour, not by method: returns -1 when target is missing reads better than testBinarySearch1. The name is your documentation.
  • Arrange / Act / Assert (AAA): every test has the same three-section shape, separated by blank lines. Deviations are loud.
  • One assertion per test is a heuristic, not a law — the real law is “one behaviour per test”. Three related post-conditions assert one thing.

A mutant in if (x > 0) return compute(x) should kill exactly one test, not three.

Practice Trajectory

  1. Take a unit of pure logic and convert one example-style test to a property with shrinking (try fast-check for JS, hypothesis for Python). Note the first bug the fuzzer finds.
  2. Add a mutation-testing run (stryker for JS, mutmut for Python) to a critical module. Surviving mutants reveal “covered” lines nobody actually tests.
  3. Write a consumer-driven contract test for one microservice and a producer verifying it. Ship both.
  4. Audit one snapshot-heavy suite: read the last ten --updateSnapshot commits. Were they drift or behaviour change? Divide into each category.
  5. Refactor one AAA-violating test in your own code into AAA shape. Describe what the new shape made obvious.

When It’s the Right Tool

SituationTakeaway
Pure logic with edge casesProperty-based testing beats example-based
Microservices with breaking-prod historyConsumer-driven contract tests
“Coverage at 90% but bugs still ship”Mutation testing reveals covered-by-name-only lines
Generated output (HTML, JSON, JSX)Snapshot tests as change detectors, not assertions
Reviewing a test PRBehaviours first, mechanics second — a passing flaky test is worse than a failing useful one