Home Tutorials Testing

Testing

Mocking in Python Tests: unittest.mock Explained

Pyford Notes July 6, 2026 7 min read
Key points
  • A Mock object accepts any attribute access or method call and quietly records it, without needing to match a real class's shape.
  • patch() replaces an object for the duration of a test only, and restores the original automatically afterwards.
  • assert_called_with() and similar methods check exactly how a mock was called, catching wrong arguments as well as missed calls.
  • Mocking too much of a test's own code under test, rather than its external dependencies, produces tests that pass even when the real behaviour is broken.

Why tests need a fake instead of the real thing

A function that sends an email, hits an external API, or writes to a production database is awkward to test directly — you don't want a test run to actually email someone or depend on a network call succeeding. Mocking replaces that dependency with a fake object for the duration of the test, so the test can check that your code called the dependency correctly, without the dependency actually doing anything.

Mock and MagicMock basics

A Mock object accepts any attribute access or method call, records what happened, and returns another Mock by default unless told otherwise:

from unittest.mock import Mock

mock_client = Mock()
mock_client.send_email("[email protected]", "hello")

print(mock_client.send_email.called)          # True
mock_client.send_email.assert_called_once()   # passes silently

Nothing about mock_client needs to match a real email client's class — it accepts .send_email(...) because it accepts literally any attribute or call. MagicMock, the default when using patch(), extends this to also support Python's dunder methods like __len__ or __iter__, which a plain Mock doesn't implement automatically.

patch(): replacing an object for the duration of a test

patch() swaps out a named object with a mock for the scope of a test, then restores the original afterwards even if the test fails:

from unittest.mock import patch
import requests

def get_status(url):
    return requests.get(url).status_code

class TestGetStatus(unittest.TestCase):
    @patch("requests.get")
    def test_get_status(self, mock_get):
        mock_get.return_value.status_code = 200
        result = get_status("https://example.com")
        self.assertEqual(result, 200)
        mock_get.assert_called_once_with("https://example.com")

The decorator injects the mock as an extra argument to the test method, here mock_get. The test never makes a real HTTP request — requests.get is replaced with the mock for the duration of the test only, and the real function is back in place the moment the test method returns.

Checking a mock was called correctly

Beyond just checking that something was called, mocks let you check exactly how:

mock_client.send_email("[email protected]", subject="hello")

mock_client.send_email.assert_called_with("[email protected]", subject="hello")
print(mock_client.send_email.call_count)     # 1
print(mock_client.send_email.call_args)      # call('[email protected]', subject='hello')

assert_called_with() checks the most recent call's exact arguments and fails with a clear diff if they don't match — catching a bug where the code calls the right method with the wrong arguments, which a simple "was it called" check would miss entirely.

Controlling return values and side_effect

return_value fixes what a mock returns every time it's called; side_effect is more flexible, letting a mock raise an exception, return different values on successive calls, or run a custom function:

mock_db = Mock()
mock_db.get_user.side_effect = [
    {"id": 1, "name": "Ada"},
    {"id": 2, "name": "Grace"},
]
print(mock_db.get_user())   # {'id': 1, 'name': 'Ada'}
print(mock_db.get_user())   # {'id': 2, 'name': 'Grace'}

mock_db.get_user.side_effect = ConnectionError("db is down")
mock_db.get_user()   # raises ConnectionError

Setting side_effect to an exception class or instance makes the mock raise it when called, which is the standard way to test how your code handles a dependency failing — a path that's otherwise hard to trigger reliably against a real service.

Where mocking goes wrong

Mocking is meant for boundaries: the network, the filesystem, the clock, another team's service. Mocking the function actually under test, or mocking so much of a module's internals that the test only checks that mocks were called in the right order, produces a suite that passes even after the real logic breaks, because nothing real ever ran. A useful check when writing a mock-heavy test is to ask what would happen if the mocked dependency's actual behaviour changed shape — a renamed field, a different response format — and whether the test would still pass. If the answer is yes, the test isn't really protecting against that class of bug, and an integration test against something closer to the real dependency may be worth adding alongside it. The full patch() and Mock API is documented in the official unittest.mock reference.

A related trap is patching the wrong location. patch() replaces a name where it's looked up, not where it's originally defined, which trips people up constantly when a function is imported into another module:

# billing.py
from email_client import send_email

def charge_customer(amount):
    send_email("receipt sent")
    # ... charge logic

# test_billing.py -- WRONG: patches the original, unused location
@patch("email_client.send_email")
def test_charge(self, mock_send):
    ...

# CORRECT: patches the name as billing.py actually sees it
@patch("billing.send_email")
def test_charge(self, mock_send):
    ...

Because billing.py imported send_email into its own namespace, patching email_client.send_email leaves the copy that billing.py is actually calling untouched. The rule of thumb is to patch the string that matches where the name is used, not where it was originally written.