1.Contract tests, integration tests, and the four test doubles
A **contract test** checks that the request you send and the response you can parse match what the provider actually promises — it runs against a recorded or stubbed response and is fast. An **integration test** talks to the provider's sandbox for real, which is slow and can fail for reasons that have nothing to do with your code. You need both, and they answer different questions: the contract test asks "does my code handle this shape correctly", the integration test asks "is this shape still what they send". A **test double** is whatever you put in the provider's place — a stub returns a canned answer, a fake is a working simplified version, a spy records calls, and a mock asserts on them.
Key features
- Contract test: your code against a stubbed provider — fast, deterministic, runs on every commit
- Integration test: your code against the real sandbox — slow, flaky, and the only thing that can tell you a stub has gone stale
- Stub → canned response · Fake → working simplified implementation · Spy → records calls · Mock → asserts on calls
- Django ships a fake for email already: the `locmem` backend fills `mail.outbox`, so no email provider needs mocking in tests
- `responses` registers per-URL behaviour and exposes `responses.calls`, each entry carrying `.request` and `.response`
- `RequestsMock(assert_all_requests_are_fired=True)` fails a test that registered a stub it never used — a dead stub is a lie about coverage
The four doubles, and when each is the right one
- Contract test — stubbed provider
- Runs on every commit, in milliseconds
- You choose the response, including the 402 and the timeout
- Catches: your code mishandling a shape
- Cannot catch: the provider changing that shape
- Deterministic — no network, no rate limits, no card expiry
- Integration test — real sandbox
- Runs nightly, tagged, allowed to fail loudly
- The provider chooses the response
- Catches: a renamed field, a changed status code, a new required header
- Cannot replace contract tests — you cannot make it produce every branch
- Its job is to invalidate your stubs, not to cover your logic
Command
@responses.activate
def test_x():
responses.post(url, json={...}, status=200) # stub
...
assert len(responses.calls) == 1 # spyContract tests answer "does my code handle this shape", integration tests answer "is this still the shape" — you need both, and the second one exists to invalidate the first one's stubs, not to cover your branches. Keep sandbox tests few, tagged and scheduled; keep stubbed tests exhaustive, because only a stub gives you a 500 or a timeout on demand. Put the double at the HTTP boundary rather than on your own client class, or the URL, headers and timeout you actually ship go untested.