API Testing / Basic: trustworthy API observations / Lesson 5
Build isolated fixtures and deterministic tests
Control state, clocks, identifiers, and dependencies without using configured services.
On this page
Lesson goals
- Design a minimal fixture that makes a failure interpretable.
- Prevent state leakage and accidental dependency calls in local tests.
Choose the smallest meaningful world
A fixture is the controlled world needed to exercise a claim. For a reading-list rule, two readers and two books may be enough to distinguish ownership and availability. A large copied dataset adds irrelevant facts, privacy risk, and failure ambiguity. Use invented records with explicit names that reveal their role. Keep the fixture close to the case or provide a documented builder with safe defaults. A builder should make the decisive difference visible; hiding ownership inside a generic convenience function makes authorization cases hard to review.
Create fresh mutable state for each case
Tests should not depend on which neighbor ran first. Mutable module-level dictionaries, reused clients, and cached identifiers can connect cases unintentionally. Construct a fresh fixture for each case and verify cleanup for any resource the case actually owns. The example creates two independent dictionaries and mutates only one. A shallow copy would still share nested lists, so deepcopy is used for this tiny fixture. In a real application, choose isolation mechanisms appropriate to the actual boundary; the local example does not exercise transactions or a database engine.
from copy import deepcopy
BASE = {"reader-a": {"entries": ["book-1"]}}
first = deepcopy(BASE)
second = deepcopy(BASE)
first["reader-a"]["entries"].append("book-2")
assert second["reader-a"]["entries"] == ["book-1"]
assert BASE["reader-a"]["entries"] == ["book-1"]
print("nested fixtures isolated")
nested fixtures isolated
The complete program verifies independence of these in-memory objects. It neither reads a project fixture nor changes a service. Deep copying is a classroom technique here, not a universal substitute for external-resource isolation.
Inject the dependency that matters
An injected clock, identifier source, or catalog lookup makes a test deterministic without sleeping or calling a remote provider. The replacement must implement the contract actually used by the caller. A fake that always returns the desired answer can hide missing validation and wrong arguments. Record calls when the interaction is relevant, and fail clearly on unexpected use. The second example supplies a fixed lookup and checks the requested book. It is a business-boundary test; it makes no claim about HTTP connectivity, a provider SDK, or provider-side data.
calls = []
def lookup(book_id):
calls.append(book_id)
if book_id != "book-4":
raise ValueError("UNEXPECTED_BOOK")
return {"id": book_id, "available": True}
def can_add(book_id, catalog_lookup):
return catalog_lookup(book_id)["available"] is True
assert can_add("book-4", lookup) is True
assert calls == ["book-4"]
print("lookup boundary checked")
lookup boundary checked
This complete program checks the requested identifier and the chosen availability decision. The replacement is local and deliberately narrow. Its passing result must not be reported as evidence that a real catalog provider honors the same contract.
Make variability replayable
Random inputs can broaden coverage, but an unrecorded failure is expensive to reproduce. Preserve the seed, generator version, and smallest failing input when using a property-testing tool. A seed alone may not reproduce results after an implementation or runtime change. Time-based identifiers also collide under parallel execution when the assumed precision is too low. Prefer explicit per-case identifiers or an injected deterministic sequence in unit fixtures. Avoid solving collisions by adding arbitrary sleeps; that hides the shared-state design and creates slower, less reliable tests.
Distinguish isolation from realism
An isolated test is easier to reason about, but every replacement leaves a boundary untested. Keep a dependency inventory showing what is real, simulated, or absent. Later integration checks in an approved disposable environment can validate adapters and provider assumptions. Do not quietly point a failing local test at a configured database or service to make it more realistic. Fixture creation and cleanup must remain within the resources explicitly authorized for the task. For this course, all executable examples stay in temporary storage or memory with standard-library tools only.
Common errors and fixes
A test fails only after another test
- Why it happens
- Mutable nested fixture state is shared.
- How to fix it
- Create independent state and reproduce the failing order.
- Prevent it next time
- Review fixture ownership and prohibit accidental global mutation.
A fake accepts the wrong book identifier
- Why it happens
- The replacement ignores its arguments.
- How to fix it
- Reject unexpected identifiers and assert the relevant call.
- Prevent it next time
- Make fake behavior match the used dependency contract.
Sleep makes the suite appear stable
- Why it happens
- Timing masks a shared identifier or state collision.
- How to fix it
- Control the clock and identifier source and remove cross-case dependencies.
- Prevent it next time
- Treat timing workarounds as diagnostic clues rather than fixes.
Check your understanding
1. A shallow fixture copy still leaks added entries. Explain and repair it.
Show worked solution
The outer dictionaries are different but their nested list is still shared. Use a fresh nested structure or a deliberate deep copy for this small fixture. Verify the untouched baseline after mutation. For external storage, select an actual resource-isolation mechanism rather than assuming copying a Python object isolates the system.
2. A dependency fake always returns available=True. What evidence is missing?
Show worked solution
It does not establish that the correct identifier was requested or that unavailable and missing books are handled. Add distinct local outcomes and explicit argument checks. Document that provider transport and serialization remain outside the test, then assign those questions to separately authorized adapter checks.
Key takeaways
- Minimal fixtures expose the decisive difference.
- Fresh state and injected dependencies make failures reproducible.
- A fake verifies only the boundary it deliberately models.