Tests often need to replace real dependencies to isolate the code under test. These replacements are called test doubles, a term coined by Gerard Meszaros in xUnit Test Patterns, and come in five varieties. A dummy object is a placeholder passed around but never used, often just filling required parameters. A stub provides canned responses to calls and is used for state verification, where the test checks the result of the system under test. A spy wraps a real function so it executes normally while recording information such as call count, arguments, and return values. A mock is pre-programmed with expectations and is used for behavior verification, asserting that specific methods were called with specific arguments. A fake is a working implementation with shortcuts, such as an in-memory database, that is more realistic than a stub but lighter than a real dependency.
Distinguishing between mocks and stubs is one of the more common points of confusion in testing. A stub simply answers calls with predefined values, allowing the test to inspect the output of the system under test; for example, a getUser stub might always return { name: "Alice" }. A mock, in contrast, asserts how the system under test interacted with it; the test might verify that getUser was called exactly once with ID 5. Mocks and stubs therefore answer different questions: mocks verify behavior, while stubs set up state.
Spies occupy a middle ground. Rather than replacing the implementation as mocks and stubs do, a spy wraps an existing function so that it can be observed without altering its behavior, which is useful for verifying side effects. In Jest, jest.spyOn(obj, 'method') creates a spy that still runs the original code while recording each call. Fakes and dummies also have distinct uses: fakes such as a HashMap-backed FakeUserRepository provide working logic without real dependencies, while dummies are simpler still, used only to satisfy method signatures. Beyond these isolated doubles, contract testing verifies that services agree on their interfaces. In a consumer-driven approach, the consumer defines the expected interactions, and the provider verifies its implementation against those contracts; the popular Pact framework enables this kind of testing without requiring full end-to-end integration.