A test for place_order needs a payment gateway, an SMS client, and a tax calculator. Wire in the real ones and every test run charges a card and texts a phone number. A unit test is supposed to run in milliseconds with no network, so all three have to be swapped for something else. That “something else” comes in five kinds, each solving a different problem, and treating them as interchangeable is why “just mock it” arguments in code review go in circles.
What is a test double
Gerard Meszaros coined “test double” in xUnit Test Patterns as the umbrella term for anything that stands in for a real dependency during a test, the way a stunt double stands in for an actor. Film crews use different doubles for different shots: one only has to stand in the background with the right haircut, another actually does the car crash. Test doubles split the same way. Dummy, stub, spy, fake, and mock are five different jobs, and what separates them is what the test checks, not how elaborate the substitute object is.
Here is the class every example below tests. Three of its dependencies are worth replacing; the fourth turns out not to matter at all:
| |
logger only runs on the error path. That matters for the first example.
Two small stand-ins get reused across the next few tests, so they are defined once here:
| |
What is a dummy object in testing
A dummy is passed in only to satisfy a parameter the test never exercises. It does nothing and checks nothing. The happy-path test below never reaches the exception branch, so logger can be anything that survives being assigned to an attribute:
| |
object() works because nothing calls a method on it. If a later change made place_order log on the success path too, this test would fail with an AttributeError. That failure is useful: it tells you the dummy stopped being a dummy.
What is a stub in unit testing
A stub returns a canned answer so the rest of the test stays deterministic. The real tax calculator might call a tax-rate API. The test doesn’t want that, so StubTaxCalculator returns 108.00 whatever subtotal it receives. You’ve already seen it at work in the dummy test above, along with StubGateway doing the same job for charge.
Nothing checks whether total_with_tax was called, or with what argument. A stub feeds the code under test a controlled input; it never verifies an interaction. That is the line between a stub and the next three doubles.
What is a test spy
A spy records the calls made to it so the test can inspect them after the code under test has run. unittest.mock.Mock records every call automatically, so most Python suites use it as a spy even when the word never appears in the test:
| |
Nothing was configured on sms_spy before the call. It accepted whatever it was given, recorded it, and the test questioned it afterward.
What is a fake object in testing
A stub can’t tell you whether two charges in a row get two different IDs, because it only knows one value. When a test needs behavior across several calls, you want a fake: a small implementation that actually works.
| |
| |
FakeGateway has no network call and no payment processor behind it, but it keeps state the way the real one does. The test needed exactly that state, and a stub could not provide it.
What is a mock object
In Meszaros’s definition, a mock carries its expectations into the test: the rules for a valid call are fixed before the code runs, and a call that breaks them fails on the spot. unittest.mock is looser than that, since you still assert afterward, but create_autospec gets you the important half. It builds the double from a real class, so every call is checked against the real method signatures as it happens:
| |
Change place_order to call self.gateway.charge(total, currency="usd") and this test fails with a TypeError inside place_order, because Gateway.charge takes no currency argument. Now picture a plain Mock() in the same spot, with someone updating the assertion to match the new call. The test passes, and the code breaks the first time it meets the real gateway. A spy confirms that a call happened. An autospecced mock also confirms the call is one the real object would accept.
Mock vs stub vs spy vs fake: comparison table
| Double | What it does | Reach for it when |
|---|---|---|
| Dummy | Passed in to satisfy a parameter; never called | A dependency exists only because the signature requires it on this path |
| Stub | Returns one canned answer no matter the input | The test needs a fixed, deterministic value out of a dependency |
| Spy | Records calls so the test can inspect them afterward | The test needs to confirm an interaction happened, without configuring it first |
| Fake | A smaller working implementation of the real thing | The test needs real behavior across multiple calls, not one canned value |
| Mock | Built with expectations about valid calls, checked as calls happen | The test needs to verify the exact shape of a call: arguments, count, order |
gateway was a stub, a fake, and a mock in three tests of the same class. The double follows what the test checks, not the dependency.
When test doubles make tests worse
Mocking every collaborator couples the test to how the code is written instead of what it does. Add a test asserting that total_with_tax is called exactly once, then refactor place_order to call it a second time for logging: the test breaks, and nothing a user cares about has changed. The fake test above has no such problem, because it asserts on the outcome (two distinct charge IDs, the right amounts recorded) instead of the call shape.
Mocking a library you don’t own has the same problem one level up. Autospec a third-party client directly and every test that touches it depends on that library’s method signatures; a minor upgrade that renames a parameter breaks tests across the suite. Wrap the library in a small interface of your own and double that instead. When the library changes, only the wrapper does.
Sometimes the right number of doubles is zero. If the real dependency is already fast and has no side effects, like a pure function or an in-memory SQLite connection, doubling it adds indirection and catches nothing. Doubles are for dependencies that are slow, remote, non-deterministic, or have side effects you don’t want in a test run.
How to choose the right test double
Start from what the test checks, not from what feels safest to fake. If you need a predictable input so a later assertion holds, stub it. To confirm the SMS went out, spy on it. If the dependency has to behave correctly across several calls, write a fake once and reuse it wherever that dependency appears. And when a caller passing the wrong arguments should fail the test before it fails in production, use an autospecced mock.
None of this works on a class that builds its own dependencies internally, because there is nowhere to hand a double in. That is the problem dependency injection solves, and it is the first thing to fix if a class resists every double in this article. Once the seam exists, decide which double belongs at which test level. If you work test-first, you’ll reach for doubles early, since the test exists before there is real code to call.