Skip to main content

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:

  1. Isolate code - Test units independently, without dependencies
  2. Speed up tests - Avoid time-consuming operations like API calls or database queries
  3. Test edge cases - Simulate error conditions or rare scenarios
  4. 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:

python
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:

python
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:

python
# 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:

python
# 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:

python
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:

python
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:

python
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:

python
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:

python
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:

python
# 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:

python
# 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

  1. Mock at the right level: Mock at the boundaries of your system (external APIs, databases) rather than implementation details.

  2. Don't overuse mocking: If you're mocking too much, your tests might not be testing real behavior.

  3. Mock only what you need: Don't mock everything; focus on the dependencies that make testing difficult.

  4. Verify interactions: Use assert_called_with(), assert_called_once(), etc. to verify your code interacts correctly with dependencies.

  5. 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

  1. Incorrect patch paths: When using @patch, make sure you're patching the object where it's imported, not where it's defined.

  2. Over-mocking: This leads to tests that pass but don't verify that your system works correctly with real dependencies.

  3. Not resetting mocks: In setUp/tearDown methods, remember to reset or recreate mocks if they're reused across tests.

  4. 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

Exercises

  1. Write a test for a function that uses the requests library to fetch data from an API.

  2. Create a mock for a database connection and test code that inserts and retrieves data.

  3. Test a function that uses the datetime module by mocking datetime.now() to return a fixed date.

  4. Practice using side_effect to make a mock raise different exceptions under different conditions.

  5. 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! :)