Course outline

API Testing / Basic: trustworthy API observations / Lesson 5

Build isolated fixtures and deterministic tests

Control state, clocks, identifiers, and dependencies without using configured services.

About 4 min readContent reviewed
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.

python Check nested fixture independence
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")
Expected output
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.

python A controlled catalog dependency
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")
Expected output
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.

Official references