When to mock
The final act of mastering testing isn't just knowing how to mock, but understanding when to mock. Over-mocking can lead to brittle tests that provide a false sense of security, while a lack of mocks can make your test suite slow and unreliable. This article will conclude our series by clarifying the different types of test doubles and providing a robust framework for when to use each one, including when a real dependency is the better choice.
The Different Kinds of Test Doubles
The term "mock" is often used as a catch-all, but it's more accurate to refer to a family of objects called test doubles [1]. Each type serves a specific purpose, and understanding the distinction is key to writing high-quality tests.
-
Stubs: These are the simplest of the doubles. A stub is an object that provides canned answers to method calls made during a test. It doesn't contain any logic, and you don't assert on it. You use a stub to provide the data that your code needs to execute.
-
Fakes: A fake is a working, lightweight in-memory implementation of a dependency. For example, a fake database might be a dictionary that stores data, or a fake file system might use a dictionary to simulate file I/O. Fakes are used for integration-style tests where you need to verify that your code works with a simplified version of a real system.
-
Mocks: A mock is a test double that has expectations about its usage. It's used to verify that your code interacts with its dependencies in a specific way. You assert on the mock itself (e.g.,
mock.assert_called_once_with(...)
) to ensure a method was called with the correct arguments. Mocks are for verifying behavior. -
Spies: A spy is a hybrid of a stub and a mock. It is a wrapper around a real object that records method calls without changing the object's behavior. This allows you to verify that a method was called while still executing the real method's logic.
The Big Debate: To Mock or Not to Mock?
The decision to mock a dependency should be a conscious one. A good rule of thumb is: only mock what you don't own. This means external APIs, services, and file systems are great candidates for mocking because they are outside of your direct control [2].
However, for internal dependencies and core application logic, it's often better to use a fake or even the real object.
Use Fakes When
- You need to test the integration between two modules. Using a fake database allows you to test that your code correctly inserts and retrieves data without relying on a real database connection. This is an excellent middle ground between a unit test and an end-to-end test.
- The real dependency is too complex for a mock. Fakes are useful when a dependency has many methods or complex state that would be cumbersome to mock manually.
- You want to build confidence in your code's interactions with a system. A fake provides a higher degree of confidence than a mock because it behaves more like the real system.
Use Real Dependencies When
- The dependency is lightweight and fast. For example, using a real
datetime
object or a real string manipulation function is always better than mocking it, as mocking adds a layer of indirection that can hide bugs. - The test provides more value with the real dependency. Sometimes, the most valuable part of a test is verifying that a dependency works as expected. In these cases, a full integration test is a better choice.
- You are testing framework components. A test runner's built-in client for a web framework like Flask or Django provides a controlled, in-memory environment that is superior to manually mocking all the components of an HTTP request.
Putting It All Together
Let's apply these concepts to a real-world scenario.
Scenario: You have a function that processes a user's data, saves it to a database, and then sends an email.
- Mock the Email Service: Use a mock for the email client. You don't want to send real emails in your tests. You would assert that the mock's
send_email
method was called with the correctto
andsubject
arguments. - Fake the Database: Use a fake database (e.g., a dictionary-based implementation) for your test. This allows you to verify that your function correctly interacts with the database (e.g., that the user record was saved) without relying on a real database connection.
- Test the Core Logic: Don't mock the core processing logic itself. Pass in a fake database and a mock email client, but let the data processing function run as-is. This is where your most valuable test assertions will be.
By strategically choosing your test doubles, you can create a test suite that is fast and reliable, providing you with a high degree of confidence that your application works correctly without the maintenance burden of a test that is too tightly coupled to its implementation.