Python's unittest.mock
library is a powerful tool for creating test doubles
that simulate the behavior of real objects during testing.
Whether you're testing code that interacts with external services, trying to
control unpredictable dependencies, or simply looking to isolate components for
robust unit tests, understanding how to effectively use unittest.mock
can
significantly improve your testing strategy.
Let's get started!
Understanding mocking fundamentals
Mocking is a testing technique where you replace real objects in your code with controlled substitutes that simulate the behavior of the real objects.
These substitutes, commonly referred to as "mock objects" or simply "mocks," allow you to isolate the code you're testing by eliminating dependencies on external components.
Consider a scenario where your code needs to make API calls to an external service. Testing this code directly would be challenging for several reasons:
- The external service might be unavailable during testing.
- The service might return different responses each time.
- Making actual API calls would slow down your tests.
- You might incur costs for each API call during testing.
By using mock objects, you can simulate these external dependencies, giving you complete control over how they behave during your tests.
Here's a simple example of the kind of code that benefits from mocking:
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
if response.status_code == 200:
return response.json()
else:
return None
Testing this function directly would require an active internet connection and a working API server. With mocking, you can test this function without those dependencies.
The Mock class
At the core of Python's unittest.mock
library is the Mock
class. A Mock
instance can stand in for any Python object, creating attributes and methods on
demand as you access them.
To create a basic mock object, you simply instantiate the Mock
class:
from unittest.mock import Mock
# Create a basic mock object
mock_object = Mock()
# Call the mock as if it were a function
result = mock_object()
# Access attributes
attribute = mock_object.some_attribute
# Call methods
method_result = mock_object.some_method(1, 2, 'three')
print(f"Mock: {mock_object}")
print(f"Result of calling mock: {result}")
print(f"Attribute: {attribute}")
print(f"Method result: {method_result}")
Mock: <Mock id='140123456789'>
Result of calling mock: <Mock name='mock()' id='140123456788'>
Attribute: <Mock name='mock.some_attribute' id='140123456787'>
Method result: <Mock name='mock.some_method()' id='140123456786'>
As you can see, each operation on the mock returns another mock object. This is the "lazy" nature of mocks - they create attributes and methods on demand, allowing them to stand in for virtually any Python object.
Inspecting mock usage
One of the most powerful features of mocks is their ability to record how they're used, which is invaluable for making assertions in your tests:
from unittest.mock import Mock
# Create a mock
mock_service = Mock()
# Use the mock
mock_service.fetch_data('user123', limit=10)
mock_service.fetch_data('user456', limit=20)
# Inspect how the mock was used
print(f"Called: {mock_service.fetch_data.called}")
print(f"Call count: {mock_service.fetch_data.call_count}")
print(f"Last call args: {mock_service.fetch_data.call_args}")
print(f"All calls: {mock_service.fetch_data.call_args_list}")
Called: True
Call count: 2
Last call args: call('user456', limit=20)
All calls: [call('user123', limit=10), call('user456', limit=20)]
This information lets you verify that your code is using dependencies correctly. You can also use the built-in assertion methods:
from unittest.mock import Mock
import unittest
class TestMockAssertions(unittest.TestCase):
def test_mock_assertions(self):
mock_service = Mock()
# Call the mock
mock_service.process('data')
# Assertions
mock_service.process.assert_called() # Was it called at all?
mock_service.process.assert_called_once() # Was it called exactly once?
mock_service.process.assert_called_with('data') # Was it called with these args?
# Call it again
mock_service.process('more data')
# This would fail because it was called more than once
# mock_service.process.assert_called_once()
# But we can check the most recent call
mock_service.process.assert_called_with('more data')
if __name__ == '__main__':
unittest.main()
Running this test would pass because all the assertions match how the mock was used.
Controlling mock behavior
In most cases, you'll want your mocks to return specific values or behave in specific ways when they're called.
The simplest way to control a mock's behavior is to set its return_value
:
from unittest.mock import Mock
# Create a mock with a specific return value
mock_database = Mock()
mock_database.get_user.return_value = {'id': 123, 'name': 'Alice'}
# Now when called, it returns our specified value
user = mock_database.get_user(123)
print(user)
{'id': 123, 'name': 'Alice'}
For more complex behavior, you can use side_effect
:
from unittest.mock import Mock
from requests.exceptions import Timeout
# 1. Using an exception as a side effect
mock_requests = Mock()
mock_requests.get.side_effect = Timeout("Connection timed out")
try:
mock_requests.get("https://example.com")
except Timeout as e:
print(f"Caught exception: {e}")
# 2. Using a function as a side effect
def dynamic_response(url):
if "users" in url:
return Mock(status_code=200, json=lambda: {'users': ['Alice', 'Bob']})
else:
return Mock(status_code=404)
mock_requests.get.side_effect = dynamic_response
response1 = mock_requests.get("https://api.example.com/users")
response2 = mock_requests.get("https://api.example.com/products")
print(f"Response 1 status: {response1.status_code}, data: {response1.json()}")
print(f"Response 2 status: {response2.status_code}")
# 3. Using an iterable as a side effect
mock_db = Mock()
mock_db.get_next_item.side_effect = [1, 2, 3, StopIteration("End of data")]
print(mock_db.get_next_item()) # Returns 1
print(mock_db.get_next_item()) # Returns 2
print(mock_db.get_next_item()) # Returns 3
try:
mock_db.get_next_item() # Raises StopIteration
except StopIteration as e:
print(f"Caught: {e}")
Caught exception: Connection timed out
Response 1 status: 200, data: {'users': ['Alice', 'Bob']}
Response 2 status: 404
1
2
3
Caught: End of data
The side_effect
attribute is incredibly versatile:
- When set to an exception, it raises that exception when called.
- When set to a function, it calls that function with the same arguments and returns its result.
- When set to an iterable, it returns successive values from the iterable on each call.
Configuring mocks
You can configure a mock during initialization or using the configure_mock
method:
from unittest.mock import Mock
# Configuration during initialization
mock_db = Mock(
name="database",
return_value={"connected": True},
**{"get_user.return_value": {"id": 1, "name": "Alice"}}
)
print(f"Mock name: {mock_db._mock_name}")
print(f"Calling mock: {mock_db()}")
print(f"User data: {mock_db.get_user(1)}")
# Configuration using configure_mock
mock_api = Mock()
mock_api.configure_mock(
return_value=True,
**{
"fetch.return_value": ["item1", "item2"],
"fetch.side_effect": lambda x: ["result_" + x]
}
)
print(f"Fetch result: {mock_api.fetch('test')}")
Mock name: database
Calling mock: {'connected': True}
User data: {'id': 1, 'name': 'Alice'}
Fetch result: ['result_test']
The patch() function
The patch()
function is one of the most powerful features of unittest.mock
.
It allows you to temporarily replace objects in your code with mock objects for
the duration of a test.
The most common way to use patch()
is as a decorator:
import requests
def get_user_count():
response = requests.get("https://api.example.com/users")
if response.status_code == 200:
return len(response.json()["users"])
return 0
import unittest
from unittest.mock import patch, Mock
from user_service import get_user_count
class TestWithContextManager(unittest.TestCase):
def test_get_user_count(self):
with patch('user_service.requests') as mock_requests:
# Configure the mock
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"users": ["user1", "user2", "user3"]}
mock_requests.get.return_value = mock_response
# Call the function we're testing
count = get_user_count()
# Assertions
mock_requests.get.assert_called_once_with("https://api.example.com/users")
self.assertEqual(count, 3)
# Outside the context manager, the original requests module is restored
if __name__ == '__main__':
unittest.main()
Sometimes you only need to patch a specific attribute or method of an object.
For this, you can use patch.object()
:
from datetime import datetime
def is_weekday():
today = datetime.today()
# Weekday returns 0 (Monday) through 6 (Sunday)
return (0 <= today.weekday() < 5)
import unittest
from unittest.mock import patch
from datetime import datetime
from datetime_example import is_weekday
class TestIsWeekday(unittest.TestCase):
def test_is_weekday_on_monday(self):
# Create a Monday (weekday=0)
monday = datetime(2023, 4, 3)
with patch.object(datetime, 'today', return_value=monday):
self.assertTrue(is_weekday())
def test_is_weekday_on_saturday(self):
# Create a Saturday (weekday=5)
saturday = datetime(2023, 4, 8)
with patch.object(datetime, 'today', return_value=saturday):
self.assertFalse(is_weekday())
if __name__ == '__main__':
unittest.main()
Understanding where to patch
One of the most confusing aspects of using patch()
is knowing exactly where to
patch. The rule is simple: patch where an object is looked up, not where it's
defined.
Consider this example:
import services
def main():
data = services.fetch_data()
return process_data(data)
def process_data(data):
return len(data)
def fetch_data():
# In reality, this might make API calls or database queries
return ["item1", "item2", "item3"]
When testing main()
, you would patch services.fetch_data
in the app
module, not in the services
module:
import unittest
from unittest.mock import patch
import app
class TestApp(unittest.TestCase):
@patch('app.services.fetch_data')
def test_main(self, mock_fetch):
mock_fetch.return_value = ["test_item"]
result = app.main()
self.assertEqual(result, 1)
mock_fetch.assert_called_once()
if __name__ == '__main__':
unittest.main()
This can be especially tricky when dealing with imports. For example, if you import a function directly:
from services import fetch_data
def main():
data = fetch_data()
return len(data)
In this case, you would patch direct_import.fetch_data
:
import unittest
from unittest.mock import patch
import direct_import
class TestDirectImport(unittest.TestCase):
@patch('direct_import.fetch_data')
def test_main(self, mock_fetch):
mock_fetch.return_value = ["test_item"]
result = direct_import.main()
self.assertEqual(result, 1)
mock_fetch.assert_called_once()
if __name__ == '__main__':
unittest.main()
MagicMock and other specialized mocks
Python's unittest.mock
provides MagicMock
, a subclass of Mock
that
implements default magic methods:
from unittest.mock import Mock, MagicMock
# Regular Mock doesn't implement __len__ by default
regular_mock = Mock()
try:
len(regular_mock)
except TypeError as e:
print(f"Error with Mock: {e}")
# MagicMock implements __len__ and returns 0 by default
magic_mock = MagicMock()
print(f"Length of MagicMock: {len(magic_mock)}")
# You can customize magic method behavior
magic_mock.__len__.return_value = 42
print(f"New length of MagicMock: {len(magic_mock)}")
Error with Mock: object of type 'Mock' has no len()
Length of MagicMock: 0
New length of MagicMock: 42
MagicMock
is often the better choice for general-purpose mocking, especially
when you need to mock objects that use magic methods.
Other specialized mocks
The unittest.mock
library also provides several specialized mocks for specific
use cases:
- AsyncMock (Python 3.8+): For mocking coroutines and async functions.
- PropertyMock: For mocking properties in classes.
- NonCallableMock/NonCallableMagicMock: Versions that can't be called like functions.
from unittest.mock import PropertyMock, patch
class User:
@property
def name(self):
# In reality, this might access a database
return "Default Name"
# Test the property
with patch('__main__.User.name', new_callable=PropertyMock) as mock_name:
mock_name.return_value = "Mock Name"
user = User()
print(f"User name: {user.name}") # Will print "Mock Name"
User name: Mock Name
Avoiding common mocking problems
One common issue with mocks is that they can make your tests pass even when your implementation is wrong. For example, if you mistype a method name in your code, the mock will happily create that attribute with no complaints.
To avoid this, you can use the spec
parameter to specify which attributes and
methods the mock should have:
from unittest.mock import Mock
# Define a class to use as a specification
class Database:
def connect(self):
pass
def query(self, sql):
pass
# Create a mock with the Database spec
mock_db = Mock(spec=Database)
# This works fine
mock_db.connect()
mock_db.query("SELECT * FROM users")
# This raises AttributeError
try:
mock_db.execute("DELETE FROM users")
except AttributeError as e:
print(f"Error: {e}")
Error: Mock object has no attribute 'execute'
Using create_autospec()
For even more rigorous specification checking, you can use create_autospec()
:
from unittest.mock import create_autospec
def function_with_arguments(arg1, arg2, kwarg1=None):
return arg1, arg2, kwarg1
# Create an autospec of the function
mock_function = create_autospec(function_with_arguments)
# This works fine (correct signature)
mock_function(1, 2, kwarg1=3)
# This raises TypeError (incorrect signature)
try:
mock_function(1) # Missing required argument
except TypeError as e:
print(f"Error: {e}")
Error: missing a required argument: 'arg2'
Autospec with patch()
You can also use autospec with patch()
by setting autospec=True
:
import unittest
from unittest.mock import patch
from datetime_example import is_weekday
from datetime import datetime
class TestAutospecPatch(unittest.TestCase):
@patch('datetime_example.datetime', autospec=True)
def test_is_weekday(self, mock_datetime):
# Set up a Monday
monday = datetime(2023, 4, 3)
mock_datetime.today.return_value = monday
self.assertTrue(is_weekday())
# This would raise TypeError because time() doesn't exist on datetime
# mock_datetime.time()
if __name__ == '__main__':
unittest.main()
Practical examples
Mocking HTTP requests
Testing code that makes HTTP requests is a common use case for mocking:
import requests
def get_current_temperature(city):
url = f"https://api.example.com/weather/{city}"
response = requests.get(url)
if response.status_code != 200:
return None
data = response.json()
return data.get("temperature")
import unittest
from unittest.mock import patch, Mock
from weather_service import get_current_temperature
class TestWeatherService(unittest.TestCase):
@patch('weather_service.requests.get')
def test_get_temperature_success(self, mock_get):
# Configure the mock response
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"temperature": 25.5}
mock_get.return_value = mock_response
# Call the function
temp = get_current_temperature("London")
# Assertions
mock_get.assert_called_once_with("https://api.example.com/weather/London")
self.assertEqual(temp, 25.5)
@patch('weather_service.requests.get')
def test_get_temperature_not_found(self, mock_get):
# Configure the mock response for a 404
mock_response = Mock()
mock_response.status_code = 404
mock_get.return_value = mock_response
# Call the function
temp = get_current_temperature("NonexistentCity")
# Assertions
self.assertIsNone(temp)
if __name__ == '__main__':
unittest.main()
Mocking file I/O
Another common use case is mocking file operations:
import json
def read_config(file_path):
try:
with open(file_path, 'r') as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
import unittest
from unittest.mock import patch, mock_open
from config_reader import read_config
import json
class TestConfigReader(unittest.TestCase):
@patch('builtins.open')
@patch('json.load')
def test_read_config_success(self, mock_json_load, mock_open_func):
# Configure mocks
mock_json_load.return_value = {"key": "value"}
mock_file = mock_open_func.return_value.__enter__.return_value
# Call the function
config = read_config("config.json")
# Assertions
mock_open_func.assert_called_once_with("config.json", "r")
mock_json_load.assert_called_once_with(mock_file)
self.assertEqual(config, {"key": "value"})
@patch('builtins.open')
def test_read_config_file_not_found(self, mock_open_func):
# Configure mock to raise FileNotFoundError
mock_open_func.side_effect = FileNotFoundError()
# Call the function
config = read_config("nonexistent.json")
# Assertions
self.assertEqual(config, {})
@patch('builtins.open')
def test_read_config_invalid_json(self, mock_open_func):
# Use mock_open to return invalid JSON
mock_open_func.side_effect = mock_open(read_data="{invalid json")
# Call the function
config = read_config("invalid.json")
# Assertions
self.assertEqual(config, {})
if __name__ == '__main__':
unittest.main()
Here, we use mock_open()
, a helper function specifically designed for mocking
file operations.
Advanced techniques
Mocking class instantiation
Sometimes you need to mock the instance that gets created when a class is instantiated:
class APIClient:
def __init__(self, api_key):
self.api_key = api_key
def get_data(self):
# In reality, this would make an API call
return ["data1", "data2"]
def fetch_api_data(api_key):
client = APIClient(api_key)
return client.get_data()
import unittest
from unittest.mock import patch, Mock
from service_client import fetch_api_data, APIClient
class TestServiceClient(unittest.TestCase):
@patch('service_client.APIClient')
def test_fetch_api_data(self, mock_api_client_class):
# Configure the mock instance
mock_instance = Mock()
mock_instance.get_data.return_value = ["mocked_data"]
# Make the mock class return our mock instance
mock_api_client_class.return_value = mock_instance
# Call the function
data = fetch_api_data("test_key")
# Assertions
mock_api_client_class.assert_called_once_with("test_key")
mock_instance.get_data.assert_called_once()
self.assertEqual(data, ["mocked_data"])
if __name__ == '__main__':
unittest.main()
Mocking context managers
To mock objects that are used as context managers (with the with
statement),
you need to mock the __enter__
and __exit__
methods:
class DatabaseConnection:
def __enter__(self):
# In reality, this would connect to a database
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# In reality, this would close the connection
pass
def execute(self, query):
# In reality, this would execute a database query
return ["result1", "result2"]
def get_user_names():
with DatabaseConnection() as conn:
return conn.execute("SELECT name FROM users")
import unittest
from unittest.mock import patch, MagicMock
from connection_manager import get_user_names, DatabaseConnection
class TestConnectionManager(unittest.TestCase):
@patch('connection_manager.DatabaseConnection')
def test_get_user_names(self, mock_db_class):
# Configure the mock context manager
mock_connection = MagicMock()
mock_connection.execute.return_value = ["Alice", "Bob", "Charlie"]
# Configure the __enter__ method to return our mock connection
mock_db_class.return_value.__enter__.return_value = mock_connection
# Call the function
names = get_user_names()
# Assertions
mock_connection.execute.assert_called_once_with("SELECT name FROM users")
self.assertEqual(names, ["Alice", "Bob", "Charlie"])
if __name__ == '__main__':
unittest.main()
Mocking coroutines and async code
Starting from Python 3.8, unittest.mock
provides AsyncMock
for mocking async
functions:
import aiohttp
import asyncio
async def fetch_user_data(user_id):
async with aiohttp.ClientSession() as session:
async with session.get(f"https://api.example.com/users/{user_id}") as response:
if response.status == 200:
return await response.json()
return None
import unittest
from unittest.mock import patch, AsyncMock
import asyncio
# Python 3.8+ code
from async_service import fetch_user_data
class TestAsyncService(unittest.IsolatedAsyncioTestCase): # Python 3.8+
@patch('async_service.aiohttp.ClientSession')
async def test_fetch_user_data(self, mock_session_class):
# Configure the mock session
mock_session = AsyncMock()
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value={"id": 123, "name": "Alice"})
# Configure the context managers
mock_session_class.return_value.__aenter__.return_value = mock_session
mock_session.get.return_value.__aenter__.return_value = mock_response
# Call the function
data = await fetch_user_data(123)
# Assertions
mock_session.get.assert_called_once_with("https://api.example.com/users/123")
self.assertEqual(data, {"id": 123, "name": "Alice"})
if __name__ == '__main__':
unittest.main()
Best practices
When using mocks, keep these best practices in mind:
Don't mock what you don't own: Try to avoid mocking third-party libraries directly. Instead, create thin wrapper classes that you can mock.
Keep mocks simple: Only mock what you need, and keep your mock configurations as simple as possible.
Use specifications: Always use
spec
orautospec
to ensure your mocks match the interface of the real objects.Balance unit and integration tests: While mocking is great for unit tests, don't forget to include integration tests that use real dependencies.
Make tests readable: Clear and readable tests are more maintainable. Use helper methods to set up complex mocks.
Here's an example of wrapping a third-party library for easier mocking:
import requests
class WeatherAPI:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = "https://api.weather.com"
def get_temperature(self, city):
url = f"{self.base_url}/temperature/{city}?key={self.api_key}"
response = requests.get(url)
if response.status_code == 200:
return response.json()["temperature"]
return None
# Now in your application code, use this wrapper:
def display_weather(city, api_key):
api = WeatherAPI(api_key)
temp = api.get_temperature(city)
if temp is not None:
return f"The temperature in {city} is {temp}°C"
return f"Could not retrieve temperature for {city}"
import unittest
from unittest.mock import Mock, patch
from weather_api import display_weather, WeatherAPI
class TestWeatherDisplay(unittest.TestCase):
@patch('weather_api.WeatherAPI')
def test_display_weather_success(self, mock_weather_api_class):
# Configure the mock
mock_api = Mock()
mock_api.get_temperature.return_value = 25.5
mock_weather_api_class.return_value = mock_api
# Call the function
result = display_weather("London", "fake_api_key")
# Assertions
mock_weather_api_class.assert_called_once_with("fake_api_key")
mock_api.get_temperature.assert_called_once_with("London")
self.assertEqual(result, "The temperature in London is 25.5°C")
@patch('weather_api.WeatherAPI')
def test_display_weather_failure(self, mock_weather_api_class):
# Configure the mock for failed API call
mock_api = Mock()
mock_api.get_temperature.return_value = None
mock_weather_api_class.return_value = mock_api
# Call the function
result = display_weather("NonexistentCity", "fake_api_key")
# Assertions
self.assertEqual(result, "Could not retrieve temperature for NonexistentCity")
if __name__ == '__main__':
unittest.main()
By creating a wrapper around the third-party library, you make your code more testable and also gain the ability to add features or handle errors in a consistent way.
Real-world testing scenario
Let's look at a more complex, real-world example to tie together many of the concepts we've covered. Imagine we have a user registration system that needs to:
- Validate user data
- Check if the email is already registered
- Hash the password
- Store the user in a database
- Send a confirmation email
import hashlib
import re
from datetime import datetime
class Database:
def user_exists(self, email):
# In reality, this would check a database
pass
def create_user(self, user_data):
# In reality, this would insert into a database
pass
class EmailService:
def send_confirmation(self, email, user_id):
# In reality, this would send an email
pass
def validate_email(email):
pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
return bool(re.match(pattern, email))
def hash_password(password):
# In a real system, you'd use a more secure method
return hashlib.sha256(password.encode()).hexdigest()
def register_user(email, password, name):
# Validate inputs
if not validate_email(email):
return {"success": False, "error": "Invalid email format"}
if len(password) < 8:
return {"success": False, "error": "Password too short"}
# Check if user exists
db = Database()
if db.user_exists(email):
return {"success": False, "error": "Email already registered"}
# Create user
hashed_password = hash_password(password)
user_id = f"user_{int(datetime.now().timestamp())}"
user_data = {
"id": user_id,
"email": email,
"password": hashed_password,
"name": name,
"created_at": datetime.now().isoformat()
}
db.create_user(user_data)
# Send confirmation email
email_service = EmailService()
email_service.send_confirmation(email, user_id)
return {
"success": True,
"user_id": user_id
}
Now let's write comprehensive tests for this registration system:
import unittest
from unittest.mock import patch, Mock, MagicMock
from datetime import datetime
from user_registration import register_user, validate_email, hash_password
class TestUserRegistration(unittest.TestCase):
def test_validate_email(self):
# Test valid emails
self.assertTrue(validate_email("user@example.com"))
self.assertTrue(validate_email("user.name@sub.example.co.uk"))
# Test invalid emails
self.assertFalse(validate_email("not_an_email"))
self.assertFalse(validate_email("missing@domain"))
self.assertFalse(validate_email("@missing_user.com"))
def test_hash_password(self):
# Test that hashing works and produces expected output
hashed = hash_password("password123")
self.assertEqual(len(hashed), 64) # SHA-256 produces 64 character hex string
# Test that same input produces same output
hashed2 = hash_password("password123")
self.assertEqual(hashed, hashed2)
# Test that different inputs produce different outputs
hashed3 = hash_password("password124")
self.assertNotEqual(hashed, hashed3)
@patch('user_registration.Database')
@patch('user_registration.EmailService')
@patch('user_registration.datetime')
def test_register_user_success(self, mock_datetime, mock_email_class, mock_db_class):
# Mock the datetime
mock_now = datetime(2023, 4, 1, 12, 0, 0)
mock_datetime.now.return_value = mock_now
# Mock the database
mock_db = Mock()
mock_db.user_exists.return_value = False
mock_db_class.return_value = mock_db
# Mock the email service
mock_email = Mock()
mock_email_class.return_value = mock_email
# Call the function
result = register_user("new@example.com", "password123", "New User")
# Assertions
mock_db.user_exists.assert_called_once_with("new@example.com")
self.assertTrue(mock_db.create_user.called)
# Check the user data passed to create_user
user_data = mock_db.create_user.call_args[0][0]
self.assertEqual(user_data["email"], "new@example.com")
self.assertEqual(user_data["name"], "New User")
self.assertEqual(user_data["id"], "user_1680350400")
# Check email was sent
mock_email.send_confirmation.assert_called_once_with("new@example.com", "user_1680350400")
# Check result
self.assertTrue(result["success"])
self.assertEqual(result["user_id"], "user_1680350400")
@patch('user_registration.Database')
def test_register_user_invalid_email(self, mock_db_class):
# No need to mock other dependencies as they won't be called
result = register_user("invalid-email", "password123", "New User")
# Assertions
self.assertFalse(result["success"])
self.assertEqual(result["error"], "Invalid email format")
# Database should not be accessed
mock_db_class.assert_not_called()
@patch('user_registration.Database')
def test_register_user_short_password(self, mock_db_class):
result = register_user("valid@example.com", "short", "New User")
# Assertions
self.assertFalse(result["success"])
self.assertEqual(result["error"], "Password too short")
# Database should not be accessed
mock_db_class.assert_not_called()
@patch('user_registration.Database')
@patch('user_registration.EmailService')
def test_register_user_already_exists(self, mock_email_class, mock_db_class):
# Mock the database to say the user exists
mock_db = Mock()
mock_db.user_exists.return_value = True
mock_db_class.return_value = mock_db
# Call the function
result = register_user("existing@example.com", "password123", "Existing User")
# Assertions
mock_db.user_exists.assert_called_once_with("existing@example.com")
mock_db.create_user.assert_not_called()
mock_email_class.assert_not_called()
self.assertFalse(result["success"])
self.assertEqual(result["error"], "Email already registered")
if __name__ == '__main__':
unittest.main()
This example demonstrates several best practices:
- We test each component individually (validation, hashing).
- We mock out all external dependencies (database, email service, datetime).
- We test both success and failure paths.
- We make specific assertions about the interactions with our mocks.
- We verify the data passed to our dependencies.
Final thoughts
The unittest.mock
library provides a powerful toolkit for creating isolated,
predictable tests. By replacing complex, unpredictable dependencies with
controllable mock objects, you can focus on testing your code's logic rather
than worrying about external factors. While mocking is not the solution for
every testing challenge, it's an essential technique in any developer's testing
arsenal.
Remember that the goal of mocking is not to avoid testing interactions with external systems entirely—integration tests are still crucial—but rather to make your unit tests focused, fast, and reliable. By following the best practices outlined in this article and using mocks judiciously, you can build a test suite that gives you confidence in your code while remaining maintainable and efficient.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github