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
10from 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
13class 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
8from unittest.mock import patch
# 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
13class 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
14from 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
9class 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
4def 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
10import 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
11from unittest.mock import patch
from src.payment_processor import PaymentProcessor
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
11from unittest.mock import patch
from src.payment_processor import PaymentProcessor
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.post
that 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
16from 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
7def 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
8class 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
15from 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
25class 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
21from 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:
- PaymentService: Handles the payment process.
- InventoryService: Checks and updates the inventory for the ordered items.
Conceptual Implementation1
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
28class 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
22from 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:
- Focus on Roles: The test doesn’t care about the internal state or the exact implementation of
PaymentService
orInventoryService
. It only cares that the roles they play (processing payment and managing inventory) are correctly fulfilled. - Interaction Verification: The test verifies that the correct interactions happened — i.e., the
OrderProcessor
checked inventory, processed payment, and then updated the inventory. - 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
32import 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
6class 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
19from unittest.mock import patch
from src.website_data_fetcher import WebsiteDataFetcher
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
12class 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
14class 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 theMessageFormatter
. - 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
15class 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 specificMessageFormatter
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
14class 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
- Link to Example Code on Github
- tl;dw: Stop mocking, start testing
- Stop Mocking, Start Testing
- Fast tests for slow services: why you should use verified fakes
- Lisa Roach - Demystifying the Patch Function - PyCon 2018
- Edwin Jung - Mocking and Patching Pitfalls - PyCon 2019
- Growing Object-Oriented Software, Guided by Tests
- Talk: Harry Percival - Stop Using Mocks (for a while)