Common Mocking Problems & How To Avoid Them (+ Best Practices)

As a Python developer, you may have written non-deterministic code or code that has external dependencies.

One of the simplest ways to test this type of code is mocking.

But what happens when you use too much mocking? How does it affect code refactoring?

What if you change the underlying storage layer or ORM (say SQL to NoSQL), does it break the mocks? What about changes to implementation logic?

How do you capture changes in the external system if you’re using mocking? (e.g. API provider changes schema).

In this article, you’ll learn about common mocking problems, the state it puts you in (often called Mock Hell), and what to do.

You’ll gain insight into poor mocking design and learn mocking best practices to write easy-to-maintain code.

Importantly, by reading this article, you’ll avoid common mocking traps that most new developers fall into.

Let’s get started.

Mock and Patch — A Brief Recap

Before we dive deeper into common Mocking problems, let’s recap mocks and patch.

Mocking is a powerful technique used in testing to replace real objects with mock objects that simulate the behavior of real objects.

This allows you to isolate and explore the code under test.

In Python, the unittest.mock module provides a way to create and manage these mock objects.

test_file.py

1
2
3
4
5
6
7
8
9
10
from unittest.mock import Mock  

def test_mock():
# Create a mock object
mock_obj = Mock()
# Set return value
mock_obj.some_method.return_value = "mocked value"
# Call the method
result = mock_obj.some_method()
assert result == "mocked value"

Patching
While Mock objects create stand-alone mock instances, whose interactions and calls can be verfied, patch is a feature that temporarily replaces the real objects in your code with mock objects during the test execution.

This is especially useful for mocking global objects, functions, or classes within modules. Below is an example of patching.

src/calculator.py

1
2
3
4
5
6
7
8
9
10
11
12
13
class Calculator:  

def add(self, a, b):
return a + b

def subtract(self, a, b):
return a - b

def multiply(self, a, b):
return a * b

def divide(self, a, b):
return a / b

tests/test_file.py

1
2
3
4
5
6
7
8
from unittest.mock import patch  

@patch("src.calculator.Calculator.add"). # patching the add method
def test_patch(patched_method_add):
patched_method_add.return_value = 10
calculator = Calculator()
result = calculator.add(1, 2)
assert result == 10

In this example, we’ve patched the add method of the Calculator class to return a value of 10, no matter what the input.

Having had a refresher, let’s dive into the core of this article.

Common Problems with Mocking

Below are common problems with mocking, experienced personally, from interacting with others and from several Pycon videos.

Mocks Couple Your Test To Implementation Detail

Mocks create temporary instances of low-level objects (classes, methods) that can replicate the internal workings of the code under test, within the test itself.

This coupling may lead to brittle tests that break when the implementation changes, even if the external behavior remains the same.

Link to Example Code on Github

Let’s say we have a User class.

tests/problem1/test1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
class User:  
def get_first_name(self):
# Imagine this fetches the first name from a database
return "John"

def get_last_name(self):
# Imagine this fetches the last name from a database
return "Doe"

def get_full_name(self):
first_name = self.get_first_name()
last_name = self.get_last_name()
return f"{first_name} {last_name}"

We write a test for get_full_name so we mock out the 2 methods — get_first_name and get_last_name.

tests/problem1/test1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from unittest.mock import Mock  

def test_get_full_name():
user = User()

user.get_first_name = Mock(return_value="Jane")
user.get_last_name = Mock(return_value="Smith")

full_name = user.get_full_name()

user.get_first_name.assert_called_once()
user.get_last_name.assert_called_once()

assert full_name == "Jane Smith"

This works! (for now).

Let’s say somebody comes along and decides there’s a more efficient way to do this (one query instead of two).

tests/problem1/test2.py

1
2
3
4
5
6
7
8
9
class User:  
def get_full_name(self):
# New implementation fetches both names together
full_name = self.fetch_full_name_from_db()
return full_name

def fetch_full_name_from_db(self):
# Imagine this fetches the full name from a database
return "John Doe"

The above refactor immediately breaks the mocked test.

A better way of testing this would be

tests/problem1/test2.py

1
2
3
4
def test_get_full_name():  
user = User()
full_name = user.get_full_name()
assert full_name == "John Doe"

It’s cleaner, abstracts implementation detail, and doesn’t care how you get the full name and what database is used under the hood.

Confusing or Incorrect Patch Targets Lead To False Positives or Brittle Tests

If you’re unfamiliar with patch targets I highly recommend this video from Lisa Roach at Pycon.

Patching is a tricky concept and incorrectly patched targets can lead to a false sense of security or flakiness.

The Problem

Let’s consider a scenario where a PaymentProcessor class interacts with an external payment gateway.

We want to test the process_payment method without actually making a real payment. So we mock the requests library.

src/payment_processor.py

1
2
3
4
5
6
7
8
9
10
import requests  


class PaymentProcessor:
def __init__(self, gateway_url):
self.gateway_url = gateway_url

def process_payment(self, amount):
response = requests.post(f"{self.gateway_url}/pay", json={"amount": amount})
return response.json().get("status") == "success"

We might be tempted to patch requests.post to simulate a successful payment:

tests/problem2/test_payment_processor.py

1
2
3
4
5
6
7
8
9
10
11
from unittest.mock import patch  
from src.payment_processor import PaymentProcessor

@patch("requests.post")
def test_process_payment(mock_post):
mock_post.return_value.json.return_value = {"status": "success"}
processor = PaymentProcessor("https://fakegateway.com")
result = processor.process_payment(100)

assert result is True
mock_post.assert_called_once_with("https://fakegateway.com/pay", json={"amount": 100})

The test above might work fine, but there’s a lurking problem.

Patching standard modules like requests globally can be dangerous.

If the requests.post method is called elsewhere in your code, this test could unintentionally mock those calls as well.

It also creates a brittle dependency on the global requests library.

Correct Approach

Instead of patching the requests.post method globally, we should patch the method specifically where it’s used in our PaymentProcessor class.

This isolates our test, ensuring it only affects the behavior we intend to mock.

tests/problem2/test_payment_processor_correct.py

1
2
3
4
5
6
7
8
9
10
11
from unittest.mock import patch  
from src.payment_processor import PaymentProcessor

@patch("src.payment_processor.requests.post")
def test_process_payment(mock_post):
mock_post.return_value.json.return_value = {"status": "success"}
processor = PaymentProcessor("https://fakegateway.com")
result = processor.process_payment(100)

assert result is True
mock_post.assert_called_once_with("https://fakegateway.com/pay", json={"amount": 100})

In this version, we’ve explicitly patched requests.post within the context of the PaymentProcessor module.

This ensures that our test is isolated and that any other use of requests.post in different parts of the codebase remains unaffected by this test.

Even better is to create an adaptor around requests.postthat can be patched directly. We’ll cover this as one of the best practices below.

Note — Always patch the exact location where the method or object is being used, not where it was originally defined.

Mocks Fail To Respect Method Signature

When creating mocks, it’s crucial to ensure that your mock objects accurately reflect the objects they’re replacing.

Failing to do so can lead to tests that pass incorrectly or, worse, catastrophic failures in production.

Let’s consider an example,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from unittest.mock import Mock  


class PaymentProcessor:
def process_payment(self, amount, currency="USD"):
# Imagine this sends a payment request
return True


def test_process_payment():
mock_processor = Mock(PaymentProcessor)
mock_processor.process_payment.return_value = True

# Notice that we're calling the method with an extra argument not in the real method
result = mock_processor.process_payment(100, "USD", "EXTRA_ARG")
assert result is True

This test passes without any issues, even though the process_payment method in the real PaymentProcessor class does not accept three arguments.

The mock object allows this because it doesn’t enforce the method signature.

Imagine if you refactor your code, relying on this test.

The test gives you a false sense of security that everything works as expected.

However, in production, when this method is called with the wrong arguments, Python will raise a TypeError, causing a failure that your tests didn’t catch.

This kind of error is particularly insidious because it might not surface until it’s too late, potentially in a critical production environment.

How autospec Solves This Problem

By using autospec=True, you can ensure that your mock objects respect the function signatures of the objects they’re replacing.

This prevents the creation of invalid method calls that don’t match the real method’s signature.

1
2
3
4
5
6
7
def test_process_payment():  
mock_processor = create_autospec(PaymentProcessor)
mock_processor.process_payment.return_value = True

# This will now raise a TypeError if we attempt to call with incorrect arguments
result = mock_processor.process_payment(100, "USD", "EXTRA_ARG")
assert result is True

If you attempt to add an extra argument as we did in the earlier example, this test would immediately raise a TypeError, alerting you to the fact that your mock is not being used correctly.

Similarly, you can also pass the autospec=True parameter to the patch function.

Mocking Low-Level Architecture Makes It Harder to Improve Design

Mocking low-level details, such as database queries, ties your tests to the exact implementation of those details.

This becomes a significant issue when you need to make architectural changes, such as switching from an SQL database to a NoSQL one, or when you want to refactor your application to expose a new interface (like moving from a CLI tool to a web-based UI).

If you’ve mocked out the SQLAlchemy ORM, you’ve tied yourself to this implementation and is more of a design issue than mocking per se.

These changes should ideally be transparent to your business logic, but if your tests are tightly coupled to the implementation, you’ll find tests breaking.

Example Scenario: Mocking Database Queries

Imagine you have a simple UserService class that interacts with an SQL database to retrieve user information.

tests/problem4/test_user_service.py

1
2
3
4
5
6
7
8
class UserService:  
def __init__(self, db_connection):
self.db_connection = db_connection

def get_user_by_id(self, user_id):
cursor = self.db_connection.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
return cursor.fetchone()

To test the get_user_by_id method, you might mock the db_connection and the cursor:

tests/problem4/test_user_service.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from unittest.mock import Mock  

def test_get_user_by_id():
mock_db_connection = Mock()
mock_cursor = mock_db_connection.cursor.return_value
mock_cursor.fetchone.return_value = {"id": 1, "name": "John Doe"}

service = UserService(mock_db_connection)
user = service.get_user_by_id(1)

assert user == {"id": 1, "name": "John Doe"}
mock_db_connection.cursor.assert_called_once()
mock_cursor.execute.assert_called_once_with(
"SELECT * FROM users WHERE id = ?", (1,)
)

While this test works, it is tightly coupled to the specific SQL query.

If you decide to switch from SQLite to MongoDB, where queries are handled differently, or even change the table name, you’ll need to rewrite this test completely.

Deep/Recursive Mocks (mocks-in-mocks)

Deep or recursive mocks, often referred to as “mocks-in-mocks,” occur when you start mocking objects that, in turn, return other mocks.

This can quickly lead to highly complex and hard-to-understand tests, making it difficult to determine what the test is actually verifying.

Imagine you have a WeatherService class that relies on a WeatherClient to fetch weather data.

The WeatherClient interacts with an external API and returns a WeatherData object.

The WeatherData object has methods to get specific details like temperature and humidity.

tests/problem5/test_weather.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class WeatherData:  
def get_temperature(self):
# Imagine this method returns the temperature
return 72

def get_humidity(self):
# Imagine this method returns the humidity
return 55


class WeatherClient:
def fetch_weather(self, location):
# Imagine this method fetches weather data from an external API
return WeatherData()


class WeatherService:
def __init__(self, client):
self.client = client

def get_weather_report(self, location):
weather_data = self.client.fetch_weather(location)
temperature = weather_data.get_temperature()
humidity = weather_data.get_humidity()
return f"The temperature is {temperature}°F and the humidity is {humidity}%."

When testing WeatherService, you might end up creating a mock for the WeatherClient and then another mock for the WeatherData that the WeatherClient returns. This leads to a chain of mocks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from unittest.mock import Mock  

def test_get_weather_report():
mock_client = Mock()
mock_weather_data = Mock()

# Set the return values for the methods of the WeatherData mock
mock_weather_data.get_temperature.return_value = 72
mock_weather_data.get_humidity.return_value = 55

# Set the return value for the fetch_weather method of the mock client
mock_client.fetch_weather.return_value = mock_weather_data

service = WeatherService(mock_client)

report = service.get_weather_report("New York")

assert report == "The temperature is 72°F and the humidity is 55%."
mock_client.fetch_weather.assert_called_once_with("New York")
mock_weather_data.get_temperature.assert_called_once()
mock_weather_data.get_humidity.assert_called_once()

In this example, the test creates a mock for WeatherClient, which is expected.

However, it then proceeds to create a mock for WeatherData, which is returned by the fetch_weather method of the WeatherClient.

This results in a chain of mocks:

The test becomes harder to follow because it’s now mocking multiple layers of the interaction.

Any change in how WeatherClient or WeatherData behaves can break the test, even if the overall functionality of WeatherService hasn’t changed.

The test starts to resemble the internal workings of the code rather than focusing on the overall behavior of WeatherService.

This kind of deep mocking can make your tests fragile and difficult to maintain.

What To Do About It?

So you may be asking, OK I get that too much mocking is not good. Where do you draw the line? Should you avoid mocking completely and have your tests use live systems?

The answer is not black and white. But here are some useful pointers I picked up from Pycon and other resources.

Mocking Best Practices

Below are some of the recommended practices compiled from a wide variety of sources on Mocking.

Mock Roles Not Objects

In this paper, Freeman and Price talk about mocking roles, not objects.

From the abstract,

Mock Objects is an extension to Test-Driven Development that supports good Object-Oriented design by guiding the discovery of a coherent system of types within a code base. It turns out to be less interesting as a technique for isolating tests from third-party libraries than is widely thought.

The paper suggests to concentrate on the interactions between objects, not their state.

You should think about the roles that objects play in the interactions, rather than just the objects themselves.

This approach focuses on verifying that objects collaborate in the right way to fulfill their roles in a system, rather than merely testing the specific methods or properties of the objects.

Edwin Chung explained this concept very well at the Pycon Cleveland 2019 conference.

Before going further, we must look at other test doubles beyond mocks and patches.

Credit — https://speakerdeck.com/pycon2019/edwin-jung-mocking-and-patching-pitfalls?slide=80

The above table summarizes the different test doubles in mocking.

Key points to keep in mind — Mocks are NOT stubs.

When we use the patch function with return_value or side_effect , we’re essentially using a stub (even though we’re using the Mock library).

When applying “Mock Roles, Not Objects,” you create mocks not to replicate the internal state or the methods of the object, but to verify the interactions between objects based on the roles they play.

Let’s look at an example to understand this.

Consider a simple example involving an OrderProcessor that processes customer orders. The OrderProcessor interacts with two collaborators:

  1. PaymentService: Handles the payment process.
  2. InventoryService: Checks and updates the inventory for the ordered items.

Conceptual Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class PaymentService:  
def process_payment(self, amount):
# Imagine this interacts with a payment gateway
return True

class InventoryService:
def check_inventory(self, item, quantity):
# Imagine this checks the inventory
return True

def update_inventory(self, item, quantity):
# Imagine this updates the inventory
pass

class OrderProcessor:
def __init__(self, payment_service, inventory_service):
self.payment_service = payment_service
self.inventory_service = inventory_service

def process_order(self, item, quantity, amount):
if not self.inventory_service.check_inventory(item, quantity):
return "Out of stock"

if not self.payment_service.process_payment(amount):
return "Payment failed"

self.inventory_service.update_inventory(item, quantity)
return "Order processed"

When testing OrderProcessor, instead of focusing on the specific methods of PaymentService and InventoryService, we’ll mock these collaborators based on the roles they play in the order processing interaction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from unittest.mock import Mock  

def test_order_processor():
# Mock the roles
payment_service = Mock()
inventory_service = Mock()

# Setup role behaviors
inventory_service.check_inventory.return_value = True
payment_service.process_payment.return_value = True

# Create the OrderProcessor with mocked roles
order_processor = OrderProcessor(payment_service, inventory_service)

# Act
result = order_processor.process_order("item1", 2, 50.0)

# Assert the roles fulfilled their interactions correctly
assert result == "Order processed"
inventory_service.check_inventory.assert_called_once_with("item1", 2)
payment_service.process_payment.assert_called_once_with(50.0)
inventory_service.update_inventory.assert_called_once_with("item1", 2)

Key Points:

  1. Focus on Roles: The test doesn’t care about the internal state or the exact implementation of PaymentService or InventoryService. It only cares that the roles they play (processing payment and managing inventory) are correctly fulfilled.
  2. Interaction Verification: The test verifies that the correct interactions happened — i.e., the OrderProcessor checked inventory, processed payment, and then updated the inventory.
  3. Decoupling: By focusing on roles and interactions, the test is decoupled from the specific implementation details of the services, making it more resilient to changes in those implementations.

Another example of Roles from the slide deck

  • Source
  • Sink
  • Parser

This kind of conceptual thinking is extremely powerful and helps you design your system with roles in mind from the beginning. Testing using Mocks or Fakes also becomes easier.

Use Fakes Instead of Mocks

So what should you do if you don’t want to use mocks?

Use Fakes instead!

Fakes are simplified implementations of your code’s collaborators that behave like the real thing but are much simpler and often in-memory.

Fakes are ideal when:

  • You want to simulate the behavior of a dependency in a lightweight way.
  • The real dependency is slow, non-deterministic, or difficult to set up (e.g., a database, a web service).
  • You want to avoid the overhead of setting up mocks and verifying interactions.

Let’s say you have a service that retrieves data from a website and saves it to a database.

src/website_data_fetcher.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import requests  


class WebsiteDataFetcher:
def __init__(self, db_adapter):
self.db_adapter = db_adapter

def fetch_data_from_api(self, url):
response = requests.get(url)
if response.status_code == 200:
return response.json()
return None

def fetch_and_store(self, url):
data = self.fetch_data_from_api(url)
if data:
self.db_adapter.store_data(data)
return True
return False


class RealDatabaseAdapter:
def __init__(self, db_connection):
self.db_connection = db_connection

def store_data(self, data):
cursor = self.db_connection.cursor()
cursor.execute(
"INSERT INTO api_data (name, value) VALUES (?, ?)",
(data["name"], data["value"]),
)
self.db_connection.commit()

What’s the best way to test this?

You can use a real database but you can also use a fake (which is a simple in-memory implementation — list).

tests/solution2/test_website_data_fetcher.py

1
2
3
4
5
6
class FakeDatabaseAdapter:  
def __init__(self):
self.data_store = []

def store_data(self, data):
self.data_store.append(data)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from unittest.mock import patch  
from src.website_data_fetcher import WebsiteDataFetcher


@patch.object(WebsiteDataFetcher, "fetch_data_from_api")
def test_fetch_and_store(mock_fetch_data_from_api):
# Setup the fake response from the API
mock_fetch_data_from_api.return_value = {"name": "Example", "value": 123}

# Use the fake database adapter
fake_db_adapter = FakeDatabaseAdapter()
fetcher = WebsiteDataFetcher(fake_db_adapter)

# Act: Fetch and store the data
result = fetcher.fetch_and_store("https://api.example.com/data")

# Assert: Check that the data was stored correctly
assert result is True
assert fake_db_adapter.data_store == [{"name": "Example", "value": 123}]

In the above code we’ve used a simple list as a fake implementation of the database, however you can use other in-memory DBs like SQLite, TinyDB, flat files.

It’s important to make sure your fakes and adaptors are up to date so make sure to have integration tests that check that your fakes and real dependencies are in sync.

This article covers verified fakes far better than I could.

Test Interfaces Not Implementation

When using mocks, it’s crucial to focus on testing the public interfaces of your components rather than their internal implementation details.

The question to ask is

How flexibly can the implementation under test change, without us having to change the test?

This principle ensures that your tests are resilient to changes and truly validate the behavior of your code.

If your tests are tied too closely to the internal workings of a class or function, even minor refactoring can cause tests to fail, leading to unnecessary maintenance.

By focusing on the interface — the public methods and their expected behavior — you ensure that your tests remain valid as long as the contract (interface) is honored, even if the underlying implementation changes.

Testing through the interface encourages better encapsulation in your code.

It allows the internal implementation to evolve independently of the tests, fostering cleaner and more modular code.

When you test the interface, you’re focusing on what the component is supposed to do rather than how it does it.

This approach aligns with the idea of behavior-driven development, where the goal is to ensure that the system behaves correctly from an end-user perspective.

Consider a service that calculates discounts:

1
2
3
4
5
6
7
8
9
10
11
12
class DiscountService:  
def __init__(self, discount_rate):
self.discount_rate = discount_rate

def apply_discount(self, total_amount):
# The implementation might involve complex calculations
return total_amount * (1 - self.discount_rate)

def test_apply_discount():
service = DiscountService(discount_rate=0.1)
discounted_price = service.apply_discount(100)
assert discounted_price == 90.0

The test checks whether the discount is applied correctly without concerning itself with how the discount is calculated internally.

By testing interfaces rather than implementation details, you create tests that are more robust, easier to maintain, and aligned with the real-world usage of your code.

Use Autospec To Respect Method Signature

In mocking, one of the common pitfalls is accidentally creating mock objects that don’t respect the method signatures of the objects they’re replacing.

This can lead to tests that pass under false pretenses, and later, catastrophic failures in production.

Method signatures define the contract of a function — what arguments it expects and what it returns.

When you mock a method without paying attention to its signature, you risk creating tests that do not accurately reflect how the method is used in real-world scenarios.

To avoid this issue, you can use the autospec feature provided by the unittest.mock library.

When you create a mock with autospec=True, it ensures that the mock object respects the method signatures of the object it’s replacing.

This means that if you try to call the mock with incorrect arguments, the test will fail.

Autospec with Patch

You can also apply autospec=True directly in the patch function.

This is particularly useful when you’re patching methods or functions in a module and want to ensure that your mocks respect their signatures.

By using autospec, you enforce stricter adherence to method signatures, which leads to more reliable and maintainable tests.

Use Dependency Injection

Dependency Injection (DI) is a design pattern that promotes the decoupling of components in a larger software system.

By providing dependencies from the outside rather than creating them internally, DI makes code more modular, flexible, and easier to test.

In the context of testing, DI enables you to replace real dependencies with mock objects or fakes, leading to more isolated and reliable tests.

The result is a system where components are loosely coupled, easy to modify or replace without affecting other parts of the system.

This isolation reduces the need for complex setup and teardown processes, as mocks or fakes can be injected directly into the class or function.

Let’s say we have a class GreetingService that generates a greeting message. The service depends on a MessageFormatter to format the message.

Without Dependency Injection

In this version, the GreetingService creates its own MessageFormatter instance internally:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MessageFormatter:  
def format_message(self, name):
return f"Hello, {name}!"

class GreetingService:
def __init__(self):
self.formatter = MessageFormatter()

def greet(self, name):
return self.formatter.format_message(name)

# Usage
service = GreetingService()
print(service.greet("Alice"))

Problems:

  • The GreetingService is tightly coupled to the MessageFormatter.
  • It’s hard to replace MessageFormatter with a different implementation (e.g., for testing).

With Dependency Injection

Now, let’s refactor the GreetingService to use Dependency Injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MessageFormatter:  
def format_message(self, name):
return f"Hello, {name}!"

class GreetingService:
def __init__(self, formatter):
self.formatter = formatter

def greet(self, name):
return self.formatter.format_message(name)

# Usage
formatter = MessageFormatter()
service = GreetingService(formatter)
print(service.greet("Alice"))
  • The GreetingService is no longer tightly coupled to a specific MessageFormatter implementation.
  • We can easily replace MessageFormatter with another implementation or a mock object for testing.

Testing with Dependency Injection

Here’s how you might test GreetingService using Dependency Injection and a fake MockFormatter .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MockFormatter:  
def format_message(self, name):
return f"Hi, {name}!"

def test_greeting_service():
mock_formatter = MockFormatter()
service = GreetingService(mock_formatter)

result = service.greet("Alice")

assert result == "Hi, Alice!"

# Running the test
test_greeting_service()

In this simple example, DI allows us to pass different implementations of MessageFormatter to GreetingService — both for design and testing.

This makes our code more flexible and easier to test.

Include Full System Tests to Compensate for Unit Testing Shortfalls

Unit tests typically isolate small units of code, such as functions or methods, and verify that these components behave correctly under various conditions.

However, because they focus on small, isolated parts of your application, they may not capture how these components interact with each other in a real-world scenario. This is where full system tests come in.

Also called end-end or acceptance tests, they evaluate the entire application as a whole.

They simulate real user interactions and workflows, testing how different parts of your system work together in a production-like environment.

By including full system tests, you can catch integration issues, validate end-to-end workflows, and ensure your system remains robust and reliable.

Define All Mocks In One Place

Organizing your mocks by defining them in a single, centralized location can significantly improve the readability and maintainability of your tests.

By grouping mocks together, typically within setup functions or fixtures, you make it easier to understand the dependencies and update them when necessary.

This practice also helps prevent inconsistencies and duplication across different test cases.

When you need to change the behavior of a mock, you only need to update it in one place, ensuring that all relevant tests are consistent and up-to-date.

Separate State (especially storage) from Behavior

Separating state, such as data storage, from code behavior is a good design principle that makes your code testable and flexible.

By isolating state management from the logic that operates on it, you can more easily test each component independently.

This separation allows you to substitute different storage mechanisms (e.g., switching from a database to an in-memory store) without altering the business logic.

It also simplifies mocking and stubbing during testing, as you can focus on verifying the behavior of your code without being entangled in the complexities of the underlying state management.

Where To Use Mocking

All is not doom and gloom when it comes to Mocking.

Mocking has its place and is most useful in the following conditions.

  • Mocking prevents the use of expensive resources — time, compute, money, network calls.
  • Mocking helps create deterministic tests for non deterministic code (e.g. simulate network errors, just raise error by mock client, randomness).
  • Mocking allows us to test objects whose state can’t or shouldn’t be exposed

Conclusion

We’re finally at the end of this long article on Common Mocking problems, how to avoid them and best practices.

In this article we highlighted several mocking gotchas to keep in mind — like incorrect patch targets, too much implementation detail in mocks and how mocks can fail to respect method signatures.

You also learned some good practices to keep in mind if you do decide to mock — use autospec, fakes, mock roles not objects, test interfaces, use adaptors and so on.

I hope this article gave you a lot to think about when it comes to mocking which is in fact a double edged sword.

Mocking was originally designed as a design and exploration tool that soon came to be used a Swiss army knife in unit testing.

However, now that you’re enlightened with the potential pitfalls, I recommend you start exploring alternative ways like using fakes instead. This helps you think about good design, architecture and testability from the get-go.

If you have questions, suggestions, or topics you’d like me to cover in future articles, feel free to reach out via Twitter, GitHub, or Email.

All the best!!

Additional Reading