Intermediate

You have written a function that sends emails, calls a third-party API, or reads from a database. You want to write unit tests for it, but running those tests would actually send emails, hit the live API, and modify real data. Worse, the tests would be slow, flaky (depending on network availability), and expensive (API calls cost money). The solution is mocking: replacing the real dependency with a controlled fake that behaves exactly how you tell it to.

Python’s unittest.mock module is the standard library’s built-in mocking toolkit. It ships with Python 3.3+ at no extra installation cost and integrates seamlessly with both unittest and pytest. The module provides Mock and MagicMock objects that record all calls made to them, plus a patch() decorator and context manager that temporarily swaps real objects with mocks in your code’s namespace.

This article covers the complete mocking workflow: creating basic mocks, configuring return values and side effects, using patch() to intercept dependencies, mocking HTTP requests, mocking time, and asserting on mock call history. By the end, you will be able to test any function in isolation, no matter what external systems it depends on.

unittest.mock Quick Example

Here is the pattern in its simplest form: a function that calls an external API, tested without hitting any network.

# quick_mock_example.py
import unittest
from unittest.mock import patch, MagicMock

def get_user_name(user_id: int) -> str:
    """Fetches a user name from an external API (real implementation)."""
    import requests
    response = requests.get(f"https://jsonplaceholder.typicode.com/users/{user_id}")
    response.raise_for_status()
    return response.json()["name"]

class TestGetUserName(unittest.TestCase):
    
    @patch("quick_mock_example.requests.get")
    def test_returns_user_name(self, mock_get):
        # Configure the fake response
        mock_response = MagicMock()
        mock_response.json.return_value = {"id": 1, "name": "Leanne Graham"}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
        
        # Call the function under test
        result = get_user_name(1)
        
        # Assertions
        self.assertEqual(result, "Leanne Graham")
        mock_get.assert_called_once_with(
            "https://jsonplaceholder.typicode.com/users/1"
        )

if __name__ == "__main__":
    unittest.main()

Output:

python -m pytest quick_mock_example.py -v

quick_mock_example.py::TestGetUserName::test_returns_user_name PASSED

1 passed in 0.04s

The test completes in 40 milliseconds with zero network calls. The @patch() decorator intercepts requests.get in the module under test and replaces it with a MagicMock. The mock records the call and returns exactly what we told it to. The deeper sections explain each component in detail.

Mock and MagicMock: The Foundation

A Mock object accepts any attribute access and any method call without raising errors. Every call is recorded. MagicMock is a subclass that additionally supports Python’s magic methods (__len__, __iter__, __enter__, etc.), making it suitable for mocking context managers and containers.

# mock_basics.py
from unittest.mock import Mock, MagicMock

# Any attribute access returns another Mock
m = Mock()
print(m.anything)           # 
print(m.foo.bar.baz())      # 

# Configure return values
m.get_price.return_value = 9.99
print(m.get_price())        # 9.99

# Raise an exception
m.failing_call.side_effect = ValueError("Something went wrong")
try:
    m.failing_call()
except ValueError as e:
    print(e)                # Something went wrong

# Check call history
m.calculate(10, 20)
print(m.calculate.called)               # True
print(m.calculate.call_count)           # 1
print(m.calculate.call_args)            # call(10, 20)

# MagicMock supports __len__, __iter__, context manager
mm = MagicMock()
mm.__len__.return_value = 5
print(len(mm))              # 5

mm.__iter__.return_value = iter([1, 2, 3])
print(list(mm))             # [1, 2, 3]

Output:



9.99
Something went wrong
True
1
call(10, 20)
5
[1, 2, 3]

The key distinction between Mock and MagicMock: use MagicMock by default (it covers everything Mock does plus magic methods), and only drop to Mock if you specifically want attribute access on undefined magic methods to raise AttributeError rather than silently returning another Mock.

Mock replaces real API calls with controlled responses
No network. No database. No problem. The mock says what we need it to say.

The patch() Function: Intercepting Dependencies

patch() is the workhorse of unittest.mock. It temporarily replaces an object in a specific module’s namespace for the duration of a test, then restores it afterward. The target string must be the full dotted path to the object as it is imported in the code under test — not where it is originally defined.

patch() as a Decorator

# patch_decorator.py
import unittest
from unittest.mock import patch, MagicMock

# --- Code under test ---
import os

def get_home_directory() -> str:
    return os.path.expanduser("~")

def read_config_file(path: str) -> dict:
    import json
    with open(path) as f:
        return json.load(f)

# --- Tests ---
class TestWithPatch(unittest.TestCase):
    
    @patch("patch_decorator.os.path.expanduser")
    def test_get_home(self, mock_expand):
        mock_expand.return_value = "/home/testuser"
        result = get_home_directory()
        self.assertEqual(result, "/home/testuser")
        mock_expand.assert_called_once_with("~")
    
    @patch("builtins.open", new_callable=MagicMock)
    @patch("patch_decorator.json.load")
    def test_read_config(self, mock_json_load, mock_open):
        mock_json_load.return_value = {"debug": True, "port": 8080}
        result = read_config_file("/fake/config.json")
        self.assertEqual(result["port"], 8080)
        mock_open.assert_called_once_with("/fake/config.json")

if __name__ == "__main__":
    unittest.main()

Output:

..
----------------------------------------------------------------------
Ran 2 tests in 0.003s
OK

When stacking multiple @patch() decorators, the mocks are passed to the test function in bottom-up order — the bottom-most decorator’s mock is the first argument. This is a common source of confusion. In the example above, mock_json_load comes from the lower @patch("patch_decorator.json.load") and mock_open from the upper @patch("builtins.open").

patch() as a Context Manager

Use the context manager form when you only need the mock for part of a test, or when you are not using a test class.

# patch_context.py
from unittest.mock import patch
import pytest

def send_notification(message: str, email: str) -> bool:
    """Sends an email notification. Returns True on success."""
    import smtplib
    with smtplib.SMTP("smtp.gmail.com", 587) as server:
        server.starttls()
        server.sendmail("noreply@example.com", email, message)
    return True

def test_send_notification_success():
    with patch("patch_context.smtplib.SMTP") as mock_smtp_class:
        mock_server = mock_smtp_class.return_value.__enter__.return_value
        mock_server.sendmail.return_value = {}
        
        result = send_notification("Hello!", "user@example.com")
        
        assert result is True
        mock_server.starttls.assert_called_once()
        mock_server.sendmail.assert_called_once_with(
            "noreply@example.com", "user@example.com", "Hello!"
        )

Output:

pytest patch_context.py -v

patch_context.py::test_send_notification_success PASSED

Mocking a context manager requires one extra step: you need to mock the object returned by __enter__. In the example above, mock_smtp_class.return_value is what smtplib.SMTP("smtp.gmail.com", 587) returns, and .__enter__.return_value is the server variable inside the with block. This chain is the standard pattern for any with statement mock.

Side Effects: Sequences, Exceptions, and Callables

The side_effect attribute gives you fine-grained control over what happens when a mock is called. You can make it raise exceptions, return different values on successive calls, or run a custom function.

# side_effects.py
from unittest.mock import Mock, patch
import unittest

class TestSideEffects(unittest.TestCase):
    
    def test_raise_on_third_call(self):
        """Simulate a flaky API that fails on the 3rd attempt."""
        mock_api = Mock()
        mock_api.side_effect = [
            {"status": "ok", "data": "first"},
            {"status": "ok", "data": "second"},
            ConnectionError("API unreachable"),
        ]
        
        self.assertEqual(mock_api()["data"], "first")
        self.assertEqual(mock_api()["data"], "second")
        with self.assertRaises(ConnectionError):
            mock_api()
    
    def test_dynamic_response(self):
        """Return different responses based on the input."""
        def dynamic(url):
            if "users" in url:
                return {"type": "user", "id": 1}
            return {"type": "unknown"}
        
        mock_get = Mock(side_effect=dynamic)
        self.assertEqual(mock_get("https://api.example.com/users/1")["type"], "user")
        self.assertEqual(mock_get("https://api.example.com/other")["type"], "unknown")

if __name__ == "__main__":
    unittest.main()

Output:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK

The list-based side_effect is extremely useful for testing retry logic: set it to a list where the first N elements are exceptions and the last element is the successful response. Your retry function should exhaust the exceptions and succeed on the final call. If the mock runs out of values in the list, it raises StopIteration on the next call.

side_effect list simulates retry logic
side_effect=[error, error, success]. Retry logic tested.

Asserting on Calls

After running the code under test, verify that the mock was called correctly. The assertion methods are specific enough to catch argument order mistakes and missing calls.

# assert_calls.py
from unittest.mock import Mock, call

mock_logger = Mock()

# Simulate some calls
mock_logger.info("Server started on port 8080")
mock_logger.warning("Disk usage at 85%")
mock_logger.info("Request received from 192.168.1.1")

# Basic call assertions
mock_logger.info.assert_called()                   # Was it called at all?
mock_logger.warning.assert_called_once()           # Called exactly once?
mock_logger.error.assert_not_called()              # Was it NOT called?

# Argument assertions
mock_logger.warning.assert_called_with("Disk usage at 85%")

# Assert all calls in order
mock_logger.info.assert_has_calls([
    call("Server started on port 8080"),
    call("Request received from 192.168.1.1"),
])

# Get full call history
print(mock_logger.info.call_args_list)
# [call('Server started on port 8080'), call('Request received from 192.168.1.1')]

print(mock_logger.call_count)   # Total calls across all methods
print(mock_logger.call_args_list)  # All calls in order

Output:

[call('Server started on port 8080'), call('Request received from 192.168.1.1')]
3
[call.info('Server started on port 8080'), call.warning('Disk usage at 85%'), call.info('Request received from 192.168.1.1')]

Use assert_called_once_with() (not assert_called_with()) when you need to verify both the arguments AND that the function was called exactly once. assert_called_with() only checks the most recent call’s arguments — if the mock was called 10 times, it passes as long as the last call matches.

Real-Life Example: Testing a Weather Service Client

This project tests a complete weather service client that fetches data from an external API and caches results — no network required.

# weather_service.py
import requests
from datetime import datetime

class WeatherService:
    """Fetches weather data from an external API."""
    
    BASE_URL = "https://api.open-meteo.com/v1/forecast"
    
    def __init__(self):
        self._cache = {}
    
    def get_temperature(self, latitude: float, longitude: float) -> float:
        """Returns current temperature in Celsius. Raises on HTTP error."""
        cache_key = (round(latitude, 2), round(longitude, 2))
        if cache_key in self._cache:
            return self._cache[cache_key]
        
        params = {
            "latitude": latitude,
            "longitude": longitude,
            "current_weather": True,
        }
        response = requests.get(self.BASE_URL, params=params)
        response.raise_for_status()
        
        data = response.json()
        temp = data["current_weather"]["temperature"]
        self._cache[cache_key] = temp
        return temp
    
    def is_warm(self, latitude: float, longitude: float) -> bool:
        """Returns True if temperature is above 20 C."""
        return self.get_temperature(latitude, longitude) > 20.0

# --- Tests ---
import unittest
from unittest.mock import patch, MagicMock

class TestWeatherService(unittest.TestCase):
    
    def setUp(self):
        self.service = WeatherService()
    
    @patch("weather_service.requests.get")
    def test_get_temperature_success(self, mock_get):
        mock_response = MagicMock()
        mock_response.json.return_value = {
            "current_weather": {"temperature": 23.5, "windspeed": 12.0}
        }
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
        
        temp = self.service.get_temperature(48.85, 2.35)
        self.assertEqual(temp, 23.5)
    
    @patch("weather_service.requests.get")
    def test_caches_result(self, mock_get):
        mock_response = MagicMock()
        mock_response.json.return_value = {"current_weather": {"temperature": 18.0}}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
        
        self.service.get_temperature(51.5, -0.12)
        self.service.get_temperature(51.5, -0.12)  # Second call should use cache
        
        mock_get.assert_called_once()  # API called only once
    
    @patch("weather_service.requests.get")
    def test_raises_on_http_error(self, mock_get):
        mock_response = MagicMock()
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404")
        mock_get.return_value = mock_response
        
        with self.assertRaises(requests.exceptions.HTTPError):
            self.service.get_temperature(0, 0)
    
    @patch.object(WeatherService, "get_temperature", return_value=22.0)
    def test_is_warm_true(self, mock_temp):
        self.assertTrue(self.service.is_warm(48.85, 2.35))
    
    @patch.object(WeatherService, "get_temperature", return_value=15.0)
    def test_is_warm_false(self, mock_temp):
        self.assertFalse(self.service.is_warm(48.85, 2.35))

if __name__ == "__main__":
    unittest.main(verbose=2)

Output:

test_caches_result ... ok
test_get_temperature_success ... ok
test_is_warm_false ... ok
test_is_warm_true ... ok
test_raises_on_http_error ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.008s
OK

Note the use of @patch.object() in the last two tests — it patches a method directly on a class instance, which is cleaner than patching through the module path when the class is in the same file. This pattern is especially useful when testing methods that call other methods on the same object, letting you test is_warm() independently of get_temperature().

Frequently Asked Questions

Where exactly should I patch — the import source or the import location?

Always patch where the name is used, not where it is defined. If mymodule.py does import requests and uses requests.get(), patch "mymodule.requests.get", not "requests.get". This is the single most common mocking mistake. The rule is: patch the name in the namespace of the code you are testing, because Python looks up names in their own module’s namespace at runtime.

What is autospec and when should I use it?

Pass spec=SomeClass or autospec=True to patch() to create a mock that enforces the real object’s interface. If you call the mock with wrong arguments or access an attribute that does not exist on the real object, the mock raises AttributeError or TypeError immediately. This catches tests that pass because the mock accepts anything, even when the real code would fail. Use autospec=True for production code; skip it for quick exploratory tests.

Should I use unittest.mock or pytest-mock?

Both wrap the same underlying unittest.mock machinery. The pytest-mock package provides a mocker fixture that cleans up automatically and integrates more naturally with pytest’s fixture system. If your project uses pytest, pytest-mock is slightly more ergonomic: mocker.patch("module.thing") vs the @patch decorator. Either works — the choice is stylistic.

How do I mock datetime.now()?

You cannot patch datetime.datetime.now directly because datetime is a C extension. Instead, either use the freezegun library (@freeze_time("2024-01-01") — covered in the freezegun article) or wrap datetime.now() in your own function and mock that wrapper. For most projects, freezegun is the cleaner solution since it handles all time-related calls in your code automatically.

How do I reset a mock between tests?

Call mock.reset_mock() to clear call history, or simply create a new Mock() in each test’s setUp method. If you are using @patch as a decorator, the mock is automatically fresh for each test because the decorator creates a new mock on each test run. Only manually reset mocks when you are reusing the same mock object across multiple assertions within a single test.

Conclusion

Mocking transforms slow, flaky integration tests into fast, reliable unit tests. You have covered creating Mock and MagicMock objects, configuring return values and side effects, using patch() as both a decorator and context manager, asserting on call history, and applying all of it to a realistic weather service client with five passing tests in 8 milliseconds. The key rule to remember: always patch where the name is used, not where it is defined.

Extend the weather service tests by adding a test for the cache key rounding behaviour (try coordinates that differ only in the third decimal place), and add a test that verifies the HTTP parameters passed to requests.get() using assert_called_once_with(). These edge cases are exactly what mocking was designed to cover cheaply and reliably.

For the complete API reference, see the official unittest.mock documentation.