Python Mocking
When writing tests in Python, you often need to test a piece of code that depends on other components. These dependencies might be slow, unavailable during testing, or produce unpredictable results. This is where mocking comes in - allowing you to replace these dependencies with controlled substitutes.
What is Mocking?
Mocking is a technique used in unit testing where you replace real objects with "mock" objects that simulate the behavior of the real ones. These mock objects:
- Allow you to control their behavior during tests
- Let you verify how they were used by your code
- Help isolate the specific code you're trying to test
Python's standard library includes the unittest.mock
module, which provides powerful mocking capabilities.
Why Use Mocking?
Mocking helps you:
- Isolate code - Test units independently, without dependencies
- Speed up tests - Avoid time-consuming operations like API calls or database queries
- Test edge cases - Simulate error conditions or rare scenarios
- Test without external dependencies - No need for internet connection, databases, etc.
Getting Started with unittest.mock
Python's unittest.mock
module provides the Mock
class and the patch
decorator/context manager, which are the main tools for mocking.
Basic Mock Objects
Let's start with a simple example:
from unittest.mock import Mock
# Create a mock object
mock_function = Mock()
# Use the mock
mock_function(1, 2, 3)
# Check if it was called
print(mock_function.called) # True
# Check how it was called
print(mock_function.call_args) # call(1, 2, 3)
Output:
True
call(1, 2, 3)
Configuring Mock Return Values
You can configure mocks to return specific values:
from unittest.mock import Mock
# Create and configure a mock
weather_service = Mock()
weather_service.get_temperature.return_value = 25
# Use the mock
temperature = weather_service.get_temperature("New York")
print(f"The temperature is {temperature}°C")
# Verify it was called with the right arguments
print(weather_service.get_temperature.call_args)
Output:
The temperature is 25°C
call('New York')
The patch
Decorator
The patch
decorator is used to temporarily replace classes, objects, or attributes with mock objects during a test. This is particularly useful for replacing functions or classes in the module being tested.
Basic Patching
Suppose we have a module called weather_app.py
that makes API calls:
# weather_app.py
import requests
def get_current_temperature(city):
"""Get the current temperature for a city."""
url = f"https://api.weather.com/current/{city}"
response = requests.get(url)
if response.status_code == 200:
data = response.json()
return data["temperature"]
else:
return None
def is_it_hot(city, threshold=25):
"""Determine if the current temperature is hot."""
temperature = get_current_temperature(city)
if temperature is None:
return False
return temperature > threshold
To test this without making actual API calls, we can patch the requests.get
function:
# test_weather_app.py
import unittest
from unittest.mock import patch, Mock
from weather_app import is_it_hot
class TestWeatherApp(unittest.TestCase):
@patch('weather_app.requests.get')
def test_is_it_hot_when_hot(self, mock_get):
# Configure the mock
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"temperature": 30}
mock_get.return_value = mock_response
# Test with our mock
result = is_it_hot("New York")
# Verify the result and that our mock was called correctly
self.assertTrue(result)
mock_get.assert_called_once_with("https://api.weather.com/current/New York")
@patch('weather_app.requests.get')
def test_is_it_hot_when_cold(self, mock_get):
# Configure the mock for a cold day
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"temperature": 20}
mock_get.return_value = mock_response
# Test with our mock
result = is_it_hot("New York")
# Verify the result
self.assertFalse(result)
Context Manager Approach
You can also use patch
as a context manager:
from unittest.mock import patch
import weather_app
def test_is_it_hot_context_manager():
with patch('weather_app.requests.get') as mock_get:
# Configure the mock
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"temperature": 30}
mock_get.return_value = mock_response
# Test with our mock
result = weather_app.is_it_hot("New York")
# Assertions
assert result is True
mock_get.assert_called_once()
Patching Object Attributes
You can also patch specific attributes of an object:
class WeatherDatabase:
def get_average_temperature(self, city, month):
# In a real application, this would query a database
pass
def get_seasonal_forecast(city, month):
db = WeatherDatabase()
avg_temp = db.get_average_temperature(city, month)
if avg_temp > 25:
return "It will be hot"
else:
return "It will be mild or cold"
Test using patch.object
:
from unittest.mock import patch
from weather_forecast import get_seasonal_forecast, WeatherDatabase
def test_seasonal_forecast_hot():
with patch.object(WeatherDatabase, 'get_average_temperature', return_value=30):
forecast = get_seasonal_forecast("Miami", "July")
assert forecast == "It will be hot"
def test_seasonal_forecast_cold():
with patch.object(WeatherDatabase, 'get_average_temperature', return_value=15):
forecast = get_seasonal_forecast("Boston", "January")
assert forecast == "It will be mild or cold"
Advanced Mocking Techniques
MagicMock
MagicMock
is a subclass of Mock
that implements default magic or special methods:
from unittest.mock import MagicMock
# MagicMock supports magic methods like __len__, __iter__, etc.
mock_list = MagicMock()
mock_list.__len__.return_value = 5
# Now we can use it like a regular object that supports len()
print(len(mock_list)) # 5
Mock Side Effects
Instead of a static return value, you can define dynamic behavior using side_effect
:
from unittest.mock import Mock
# Side effect can be an exception
error_mock = Mock()
error_mock.side_effect = ValueError("Something went wrong")
try:
error_mock()
except ValueError as e:
print(f"Caught an exception: {e}")
# Side effect can be a function
def side_effect_func(arg):
if arg < 0:
return "Negative"
elif arg == 0:
return "Zero"
else:
return "Positive"
number_checker = Mock()
number_checker.side_effect = side_effect_func
print(number_checker(-5)) # "Negative"
print(number_checker(0)) # "Zero"
print(number_checker(10)) # "Positive"
# Side effect can be a sequence of values
sequence_mock = Mock()
sequence_mock.side_effect = [1, 2, 3, ValueError("End of sequence")]
print(sequence_mock()) # 1
print(sequence_mock()) # 2
print(sequence_mock()) # 3
try:
sequence_mock()
except ValueError as e:
print(f"Caught an exception: {e}")
Output:
Caught an exception: Something went wrong
Negative
Zero
Positive
1
2
3
Caught an exception: End of sequence
Real-World Example: Testing a User Service
Let's look at a more comprehensive example of testing a user service that depends on a database and email service:
# user_service.py
class UserService:
def __init__(self, db_client, email_client):
self.db_client = db_client
self.email_client = email_client
def register_user(self, username, email, password):
# Check if user already exists
if self.db_client.user_exists(username):
return {"success": False, "message": "Username already taken"}
# Create user
user_id = self.db_client.create_user(username, email, password)
# Send welcome email
try:
self.email_client.send_welcome_email(email)
email_sent = True
except Exception as e:
# Log the error but continue
print(f"Failed to send email: {e}")
email_sent = False
return {
"success": True,
"user_id": user_id,
"email_sent": email_sent
}
Now let's test this using mocks:
# test_user_service.py
import unittest
from unittest.mock import Mock, patch
from user_service import UserService
class TestUserService(unittest.TestCase):
def setUp(self):
# Create mock objects
self.db_client = Mock()
self.email_client = Mock()
# Create the service with mocks
self.user_service = UserService(self.db_client, self.email_client)
def test_register_user_success(self):
# Configure mocks
self.db_client.user_exists.return_value = False
self.db_client.create_user.return_value = "user123"
# Call the method we're testing
result = self.user_service.register_user("newuser", "[email protected]", "password123")
# Assertions
self.assertTrue(result["success"])
self.assertEqual(result["user_id"], "user123")
self.assertTrue(result["email_sent"])
# Verify our mocks were called correctly
self.db_client.user_exists.assert_called_once_with("newuser")
self.db_client.create_user.assert_called_once_with("newuser", "[email protected]", "password123")
self.email_client.send_welcome_email.assert_called_once_with("[email protected]")
def test_register_user_already_exists(self):
# Configure mock to simulate existing user
self.db_client.user_exists.return_value = True
# Call the method we're testing
result = self.user_service.register_user("existinguser", "[email protected]", "password123")
# Assertions
self.assertFalse(result["success"])
self.assertEqual(result["message"], "Username already taken")
# Verify create_user was NOT called
self.db_client.create_user.assert_not_called()
self.email_client.send_welcome_email.assert_not_called()
def test_register_user_email_failure(self):
# Configure mocks
self.db_client.user_exists.return_value = False
self.db_client.create_user.return_value = "user123"
self.email_client.send_welcome_email.side_effect = Exception("Email service down")
# Call the method we're testing
result = self.user_service.register_user("newuser", "[email protected]", "password123")
# Assertions
self.assertTrue(result["success"]) # User creation still succeeds
self.assertEqual(result["user_id"], "user123")
self.assertFalse(result["email_sent"]) # But email wasn't sent
Best Practices for Mocking
-
Mock at the right level: Mock at the boundaries of your system (external APIs, databases) rather than implementation details.
-
Don't overuse mocking: If you're mocking too much, your tests might not be testing real behavior.
-
Mock only what you need: Don't mock everything; focus on the dependencies that make testing difficult.
-
Verify interactions: Use
assert_called_with()
,assert_called_once()
, etc. to verify your code interacts correctly with dependencies. -
Use patch where objects are used, not where they're defined: When patching, target the module that's using the object, not where the object is defined.
Common Pitfalls
-
Incorrect patch paths: When using
@patch
, make sure you're patching the object where it's imported, not where it's defined. -
Over-mocking: This leads to tests that pass but don't verify that your system works correctly with real dependencies.
-
Not resetting mocks: In setUp/tearDown methods, remember to reset or recreate mocks if they're reused across tests.
-
Mock too rigid: If your mock expectations are too specific, your tests might become brittle.
Summary
Python mocking is a powerful technique for isolating code during testing. The unittest.mock
module provides all the tools you need to create mock objects, configure their behavior, and verify they're used correctly.
Key concepts we covered:
- Creating basic mocks with
Mock()
- Configuring return values and side effects
- Using
patch
as a decorator and context manager - Verifying interactions with mock objects
- Real-world examples of mocking dependencies
With these techniques, you can write effective, focused unit tests that are fast, reliable, and don't depend on external systems.
Additional Resources
- Official Python unittest.mock documentation
- Python Testing with pytest - A book that includes chapters on mocking
- Real Python's Guide to Mocking
Exercises
-
Write a test for a function that uses the
requests
library to fetch data from an API. -
Create a mock for a database connection and test code that inserts and retrieves data.
-
Test a function that uses the
datetime
module by mockingdatetime.now()
to return a fixed date. -
Practice using
side_effect
to make a mock raise different exceptions under different conditions. -
Write tests for code that has multiple dependencies, using multiple patches in the same test.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)