Test Doubles: Mocks, Stubs, and Spies
A test double is a stand-in for a real component, letting you test code in isolation. Use them to fake slow dependencies like database calls or external APIs, making tests fast and predictable.
WHY IT EXISTS: To test a unit of code without its real dependencies. Real dependencies like databases or network APIs are often slow, non-deterministic, or have side effects like writing data. This makes automated tests unreliable and difficult to run. Test doubles solve this by providing predictable stand-ins.
THE MENTAL MODEL: Think of a test double as a stunt double in a movie. You want to test the main actor's dialogue (your unit of code), not their ability to survive a real explosion (the database connection). The stunt double (test double) stands in for the dangerous or complex part (the dependency), so you can safely and quickly test what you actually care about.
HOW IT WORKS: Test doubles are objects or functions that mimic the interface of a real dependency. The main types differ in how they behave and what you verify. First, Stubs provide canned answers to calls made during the test; for example, a user repository stub might always return a specific user object. Second, Mocks are objects that register which calls they receive, with pre-defined expectations you verify after the test. Third, Spies are like stubs but also record information about how they were called, letting you make assertions after the fact without setting up prior expectations.
WHEN TO USE IT: Use test doubles to isolate the code under test from its dependencies, which is the core of unit testing. Use stubs for providing state (e.g., returning a record from a fake database). Use mocks for verifying interactions (e.g., ensuring an email service was called). Use spies when you need to check interactions without the rigid expectations of a mock.
WHEN NOT TO USE IT: Avoid test doubles in integration tests, where the goal is to verify that multiple real components work together correctly. Overusing mocks in unit tests is a major footgun; if your test just mirrors your implementation logic, it becomes brittle and resistant to refactoring. A good test verifies behavior, not the exact sequence of internal function calls.
ONE CANONICAL EXAMPLE: In a Node.js Express app, you might test a UserService that depends on a UserRepository. To unit test the createUser method, you would provide a stub for UserRepository.save that simply returns a predictable user object. This allows you to test the service's logic without hitting a real database.
Read the original → en.wikipedia.org
Get five bites like this every day.
Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.