Skip to content

Test Doubles, Contracts, and Property-Based Testing

No single test style establishes all correctness. Choose a level and technique according to the defect it can reveal, and keep a smaller number of tests across real boundaries where substitutes would conceal integration risk.

Test doubles

Double Role
Stub Returns controlled values to drive a path
Fake Working simplified implementation, such as an in-memory repository
Spy Records calls for later observation
Mock Verifies a programmed interaction protocol

Use doubles at owned, stable boundaries. Mocking internal call sequences makes refactoring expensive and can test an implementation instead of behavior. An in-memory fake may not reproduce transactions, constraints, collation, or query semantics, so it cannot validate a real database integration.

Contract tests

A contract test verifies assumptions at a boundary: HTTP request and response shape, message schema, repository behavior, or a third-party client adapter. Consumer-driven contracts can improve coordination but do not prove production compatibility, performance, authorization, or side effects. Version and exercise contracts alongside at least selected real integration tests.

Property-based testing

Property-based tools generate many inputs and shrink a failure to a smaller counterexample. Properties describe invariants rather than duplicating the implementation. For sorting, useful properties include:

  • output is nondecreasing;
  • output preserves length and element multiset;
  • sorting twice equals sorting once;
  • the result agrees with a trusted oracle.

Generators define the test domain. Include empty, duplicate-heavy, extreme, and structured cases instead of assuming uniform random data is representative. Record the seed and minimized counterexample for reproducibility.

Control nondeterminism

Inject a clock, random generator, scheduler, or identifier source. Avoid sleeps; wait for an observable condition with a deadline or use virtual time. Isolate external state and make parallel-test ownership explicit. A flaky test is an unreliable signal and should be diagnosed, not retried until green.

Useful complements

Mutation testing evaluates whether tests detect small semantic changes; it is a test-suite diagnostic, not a coverage target. Static analysis, model checking, fuzzing, and formal proof cover different defect classes. Benchmarks measure performance and need their own statistical methodology.

For Spring Boot, use focused slices for framework boundaries and broader tests with real infrastructure where database or broker semantics matter. Testcontainers can provide disposable real services, but tests still need controlled data, timeouts, and deterministic assertions.