Skip to main content

When to mock

· 8 min read
Serhii Hrekov
software engineer, creator, artist, programmer, projects founder

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 correct to and subject 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.

Sources

  1. Mocks Aren't Stubs
  2. Don't Mock What You Don't Own
  3. The Ultimate Guide to Mocking in Python
  4. Python Testing Best Practices