Mocking in Unit Tests

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!