
Mocking in Unit Tests
Whether you're just starting out with testing or you've been writing tests for a while, you've likely run into a situation where your code depends on something external—a database, a web service, a file system, or even just the current time. These external dependencies can make unit testing tricky, because you want to test your code in isolation, without relying on those external factors. That's where mocking comes in.
Mocking is a technique that allows you to replace parts of your system that you're not currently testing with mock objects. These mock objects simulate the behavior of real objects, but in a controlled way. They let you focus on testing the logic of your code, without worrying about what's happening elsewhere.
Why Use Mocking?
Let’s say you have a function that sends an email. You don’t want to actually send an email every time you run your test—that would be slow, could spam people, and might fail for reasons unrelated to your code. With mocking, you can replace the actual email-sending function with a mock that just records whether it was called, and with what arguments. You can then verify that your function tried to send the correct email, without ever hitting the network.
Another common use case is when your code depends on the current time. If you write a test that checks whether a user’s subscription has expired, you don’t want to wait until tomorrow to see if it passes. Instead, you can mock the time function to return a specific datetime, making your test both predictable and fast.
Mocking helps you write tests that are:
- Fast: No waiting for slow I/O operations.
- Isolated: Tests don’t depend on external systems.
- Repeatable: The same test produces the same result every time.
- Focused: You test only the code you care about.
Basic Mocking with unittest.mock
Python’s standard library includes a module called unittest.mock
, which provides powerful tools for creating and using mock objects. The most commonly used class is Mock
. A Mock
object can be used to replace any object in your code, and it will record how it’s used.
Here’s a simple example. Suppose you have a function that checks the weather by calling an external API:
import requests
def get_weather(city):
response = requests.get(f"http://weather.com/api/{city}")
if response.status_code == 200:
return response.json()
return None
To test this without actually calling the API, you can mock the requests.get
method:
from unittest.mock import Mock, patch
def test_get_weather():
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"temp": 72, "conditions": "sunny"}
with patch('requests.get', return_value=mock_response):
result = get_weather("New York")
assert result == {"temp": 72, "conditions": "sunny"}
In this test, we create a mock_response
object that simulates what the real requests.get
would return. We set its status_code
to 200 and make its json
method return a specific dictionary. Then, we use patch
to temporarily replace requests.get
with a function that returns our mock response. When get_weather
calls requests.get
, it gets our mock instead of the real thing.
Common Mocking Patterns
There are a few common patterns you'll use again and again when mocking:
- Returning specific values: Set
return_value
on your mock to control what a function returns. - Raising exceptions: Set
side_effect
to an exception to simulate errors. - Asserting calls: Use
assert_called_with
to check that your mock was called with the expected arguments.
For example, to test error handling:
def test_get_weather_error():
mock_response = Mock()
mock_response.status_code = 404
with patch('requests.get', return_value=mock_response):
result = get_weather("Nowhere")
assert result is None
Or to simulate an exception:
def test_get_weather_exception():
with patch('requests.get', side_effect=requests.exceptions.ConnectionError):
result = get_weather("New York")
assert result is None
Mocking Attributes and Methods
Sometimes you need to mock not just a function, but an attribute or method on an object. The Mock
class makes this easy because you can assign anything to it, and it will create new attributes on the fly.
Suppose you have a class that interacts with a database:
class Database:
def connect(self):
# real connection logic
pass
def query(self, sql):
# real query logic
pass
class UserService:
def __init__(self, db):
self.db = db
def get_user(self, user_id):
self.db.connect()
return self.db.query(f"SELECT * FROM users WHERE id = {user_id}")
To test UserService
without a real database, you can mock the Database
instance:
def test_get_user():
mock_db = Mock()
mock_db.query.return_value = {"id": 1, "name": "Alice"}
service = UserService(mock_db)
result = service.get_user(1)
assert result == {"id": 1, "name": "Alice"}
mock_db.connect.assert_called_once()
mock_db.query.assert_called_with("SELECT * FROM users WHERE id = 1")
Here, we create a mock_db
and set its query
method to return a specific value. We then pass this mock to UserService
and verify that get_user
returns the expected result. We also check that connect
and query
were called correctly.
Using patch as a Decorator or Context Manager
The patch
function can be used in two ways: as a context manager (with a with
block) or as a decorator. Both are common, and you’ll see them frequently.
As a context manager:
def test_something():
with patch('module.function') as mock_function:
mock_function.return_value = 42
# test code here
As a decorator:
@patch('module.function')
def test_something(mock_function):
mock_function.return_value = 42
# test code here
The decorator form is often cleaner when you’re patching multiple things or when the entire test needs the mock.
Mocking Built-ins and Imported Modules
One tricky aspect of mocking is that you need to patch the object where it’s used, not where it’s defined. This is because when you import a function, you create a reference to it in your current module. If you patch the original module, but your code uses the local reference, the patch won’t work.
For example, if you have:
# my_module.py
import time
def get_timestamp():
return time.time()
To mock time.time
, you need to patch it in my_module
, not in the time
module:
from unittest.mock import patch
import my_module
def test_get_timestamp():
with patch('my_module.time') as mock_time:
mock_time.time.return_value = 1625097600.0
result = my_module.get_timestamp()
assert result == 1625097600.0
This is a common source of confusion, so always remember: patch where the object is used.
Advanced Mocking: Specs and Autospeccing
By default, Mock
objects will accept any method call or attribute access, which can hide bugs. For example, if you misspell a method name, the mock will happily create a new attribute instead of raising an error.
To make mocks more strict, you can use spec
or autospec
. A spec
is a list of attributes that the mock should have, or another object whose attributes the mock should mimic.
real_object = SomeClass()
mock_object = Mock(spec=real_object)
Now, if you try to access an attribute that real_object
doesn’t have, the mock will raise an AttributeError
.
autospec
goes a step further and automatically creates a spec based on the object you’re mocking. It’s especially useful when patching:
with patch('module.Class', autospec=True) as MockClass:
instance = MockClass.return_value
# instance now has the same methods as Class
This helps ensure that your mocks match the real objects closely, catching errors early.
Common Pitfalls and Best Practices
Mocking is powerful, but it can be misused. Here are some things to keep in mind:
- Don’t overmock: If you mock too much, your test might not actually test anything real. Try to mock only the external dependencies.
- Avoid mocking what you don’t own: It’s generally safer to mock your own code or well-known libraries, but be cautious with third-party code that might change.
- Keep tests readable: Too many mocks can make tests hard to understand. Use clear names and structure your tests well.
Mocking Technique | Use Case |
---|---|
return_value |
When you want a function to return a specific value. |
side_effect |
When you want to raise an exception or return multiple values. |
assert_called_with |
To verify that a mock was called with specific arguments. |
spec or autospec |
To make sure your mock has the same interface as the real object. |
Mocking in Practice: A Complete Example
Let’s look at a more complete example. Suppose we have a function that processes orders:
# order.py
import datetime
import smtplib
def process_order(order):
if order['amount'] > 1000:
send_confirmation_email(order['email'])
order['processed_at'] = datetime.datetime.now()
return True
return False
def send_confirmation_email(email):
server = smtplib.SMTP('smtp.example.com')
server.sendmail('noreply@example.com', email, 'Thank you for your order!')
server.quit()
We want to test process_order
without actually sending emails or depending on the current time. Here’s how we might write the tests:
# test_order.py
from unittest.mock import patch, Mock
import order
import datetime
def test_process_order_large():
test_order = {'amount': 1500, 'email': 'test@example.com'}
with patch('order.send_confirmation_email') as mock_send, \
patch('order.datetime') as mock_datetime:
mock_datetime.datetime.now.return_value = datetime.datetime(2023, 1, 1, 12, 0)
result = order.process_order(test_order)
assert result is True
assert test_order['processed_at'] == datetime.datetime(2023, 1, 1, 12, 0)
mock_send.assert_called_with('test@example.com')
def test_process_order_small():
test_order = {'amount': 500, 'email': 'test@example.com'}
with patch('order.send_confirmation_email') as mock_send:
result = order.process_order(test_order)
assert result is False
mock_send.assert_not_called()
In the first test, we mock both send_confirmation_email
and datetime
to control their behavior. We verify that for large orders, the email is sent and the processed time is set. In the second test, we check that for small orders, nothing happens.
When Not to Mock
While mocking is useful, it’s not always the right tool. Sometimes, it’s better to use the real thing, especially if:
- The dependency is fast and reliable (like a simple function).
- You want to test integration between components.
- The behavior is complex and hard to mock correctly.
For example, if you’re testing a function that adds two numbers, there’s no need to mock addition—just test it directly.
Conclusion
Mocking is an essential technique for writing effective unit tests. It allows you to isolate your code from external dependencies, making your tests faster, more reliable, and easier to write. With unittest.mock
, Python provides a powerful and flexible toolkit for creating mocks, patching objects, and verifying behavior.
Remember to use mocking wisely: don’t overdo it, and always aim for tests that are clear and maintainable. Happy testing!