Sponsor Of The Week: Multiplayer
Multiplayer auto-documents your system, from the high-level logical architecture down to the individual components, APIs, dependencies, and environments.
Perfect for teams who want to speed up their workflows and consolidate their technical assets.
Thank you to our sponsors who keep this newsletter free to the reader! If you want to sponsor us, click here.
Motivation
Mocks, often referred to as test doubles, are one of the most misunderstood and misused concepts in testing. Many developers use them without fully understanding their purpose, leading to brittle and unreadable tests that are hard to maintain.
Gaining a deeper understanding of test doubles—mocks, stubs, spies, fakes, and dummies—enables you to write cleaner, more effective tests, improving both the quality and reliability of your test suite.
Let's explore when and how each of these can be utilized, demonstrated through a real-life example:
Example service to test
To understand 5 types of test doubles, let's consider a real-life scenario where we want to write tests for the following UserService:
The UserService has three dependencies:
Datadog Logger: external service for logging
User Repository: for storing our entries in the database
Wage API: an external service to calculate wages for users
Now, let's see which test doubles we can use to mock out these dependencies:
Dummy
Dummies are used only for the initialization, without having any behavior. They act like placeholders, required to set up the system under test (SUT). With a dummy, we can convey our intent that the object is not directly used in the test context.
Dummies have two main forms:
Dummy values: used as simple value replacements in data fields
Dummy objects: used for more complex data types and dependencies
In our real-life example, dummies can be used for any functions of the UserService
that don’t use the logger directly but are still needed for the initialization of UserService
class:
Stub
A stub is a slightly smarter test double than a dummy as it can return some value. They are used to simulate incoming interactions, like returning hardcoded data for our SUT. A test stub is an implementation of an interface, provides a response to the system.
UserService
uses an external API to calculate the wages of the users. To isolate our code from this API, we can create a stub that returns some hardcoded wage data, allowing us to focus on the business logic of our UserService
:
Spy
A test spy is almost like a real spy from a government who silently obtains information. But instead of obtaining information from a competitor, a test spy gathers internal information from dependencies. A spy is a special kind of test double that can record and verify internal behaviors.
In our UserService
, if we are interested in how many times a function has been called, we could use a spy to collect and validate the behavior:
Mock
A Mock is a powerful beast, yet it is often the most misused. It can pre-record expectations and have configurable behaviors. Serving as a proxy for the dependency, it allows us to verify outgoing interactions, such as ensuring that a method is called with specific parameters.
Mocks can be usually created by 3rd party libraries in most languages. Although they're flexible with a lot of utilities, the syntax of using them can be quite cryptic, making the tests less readable.
For our UserService
, we could completely mock out the UserRepository, so that we can test our business logic in isolation:
Fake
A fake is a simplified and lightweight implementation of a dependency. Seemingly it behaves as a real dependency, but it just emulates business rules. Unlike mocks, they're used for verifying state rather than interactions. As they're the closest to real implementations, they're also the most powerful ones for simulating system behaviors.
For our UserService
, we can simulate the behavior of the UserRepository
by creating a fake implementation. The advantage is that your tests will be less coupled to implementation details, resulting in more robust tests that aid refactoring:
Summary
Picking the right test double is not a one-size-fits-all. It always depends on the context, type of application, type of tests, and the specific interactions you need to verify.
The choice should be guided by what you aim to achieve with your test. Ultimately, the goal is to create tests that are both effective and maintainable, providing confidence in your code without adding unnecessary complexity.
How Can I Help You?
There are two ways I could help you:
Transform Your Craft With TDD: Join 200+ engineers and master Test-Driven Development. You’ll not just learn TDD; you’ll master the best practices to produce quality software via real-world projects.
Reach 23,000+ developers and decision-makers by sponsoring this newsletter
Highly insightful!
Great job of breaking down the confusing world of test doubles.
Simply explained, Daniel!