Unit tests should not depend on databases, web APIs, file systems, or network calls. Those dependencies make tests slow, brittle, and hard to run in any environment.
When your function calls an external service, you replace that service with a mock object. A mock pretends to be the real thing, returns the values you tell it to, and records how your code called it so you can verify the interaction.
Python's unittest.mock module provides the Mock class, the patch function, and several helpers that make mocking straightforward. This article builds on the testing skills from Python unittest explained and shows you how to isolate your code from everything it depends on.
Your first mock object
A Mock object is a blank slate that accepts any attribute access and any call. You configure what it should return, then pass it to your code in place of the real dependency.
from unittest.mock import Mock
def send_notification(service, user, message):
response = service.send(user, message)
return response.status == "ok"
def test_send_notification_success():
mock_service = Mock()
mock_service.send.return_value.status = "ok"
result = send_notification(mock_service, "alice", "Hello")
assert result is True
mock_service.send.assert_called_once_with("alice", "Hello")The mock service does not actually send anything. When the code calls service.send(), the mock returns an object whose status attribute is "ok".
The test then verifies that the function called the service with the right arguments. This test runs in milliseconds with no network, no API keys, and no external service.
Configuring return values
Every Mock has a return_value attribute that controls what the mock returns when called. The default return value is a new Mock, which lets you chain attribute access without extra configuration.
mock = Mock()
mock.calculate.return_value = 42
result = mock.calculate(10, 20)
assert result == 42For nested objects, set return values at each level. This is common when mocking database clients or API wrappers that return objects with methods:
mock_db = Mock()
mock_db.connect.return_value.execute.return_value.fetchall.return_value = [
{"id": 1, "name": "Alice"}
]
rows = mock_db.connect().execute("SELECT * FROM users").fetchall()
assert rows[0]["name"] == "Alice"The mock records the full chain of calls. You can assert on the entire interaction to verify your code used the dependency correctly.
Simulating exceptions with side_effect
Real services fail. A database connection times out, an API returns a 500 error, a file permission is denied. Use the side_effect attribute to make your mock raise an exception, simulating failures so you can test your error handling code.
import pytest
def test_service_unavailable():
mock_service = Mock()
mock_service.send.side_effect = ConnectionError("Service unavailable")
with pytest.raises(ConnectionError):
send_notification(mock_service, "alice", "Hello")side_effect can also be a function that produces dynamic return values based on the arguments received. The function runs each time the mock is called, using the actual arguments passed in to decide what to return:
mock = Mock()
mock.lookup.side_effect = lambda key: {"a": 1, "b": 2}.get(key, 0)
assert mock.lookup("a") == 1
assert mock.lookup("b") == 2
assert mock.lookup("z") == 0Set side_effect to an iterable to return a different value on each call. The mock returns the next item from the iterable on each invocation:
mock = Mock()
mock.fetch.side_effect = ["first", "second", "third"]
assert mock.fetch() == "first"
assert mock.fetch() == "second"
assert mock.fetch() == "third"This is useful for testing retry logic, pagination, or any code that calls the same dependency multiple times expecting different results.
Using patch to replace dependencies
Passing mocks as arguments works when your code accepts dependencies explicitly. But much real-world code imports modules directly. The patch function temporarily replaces an object in a module so your test controls what the code sees.
from unittest.mock import patch
import my_module
@patch("my_module.send_email")
def test_user_registration(mock_send):
my_module.register_user("alice@example.com")
mock_send.assert_called_once()The decorator replaces my_module.send_email with a Mock for the duration of the test and restores it afterward. The mock is passed as an argument to the test function.
patch also works as a context manager, which is clearer when you only need the mock in part of a test:
def test_with_context_manager():
with patch("my_module.get_config") as mock_config:
mock_config.return_value = {"debug": True}
result = my_module.is_debug_mode()
assert result is TrueThe most common patch mistake is patching the wrong location. Always patch the name in the module where it is looked up. If your module has from os import getenv, you must patch my_module.getenv, not os.getenv.
Verifying mock calls
Mocks record every call, and several assertion methods let you verify the interaction. assert_called_once_with checks that the mock was called exactly once with specific arguments:
mock.process.assert_called_once_with("data", timeout=30)assert_called verifies the mock was called at least once. assert_not_called verifies it was never called, which is useful for testing that error paths skip certain operations. For example, you can confirm that a cleanup function was not invoked when the operation succeeded:
mock.cleanup.assert_not_called()assert_any_call passes if the mock was ever called with the given arguments, regardless of other calls that happened. This is useful when the same mock is called multiple times and you only care about one specific invocation:
mock.log.assert_any_call("User created", level="info")For complex interactions, inspect the call_args_list attribute, which contains every call in order. This lets you verify the exact sequence of arguments passed to the mock across multiple invocations:
assert len(mock.update.call_args_list) == 3
assert mock.update.call_args_list[0].args == (1,)These assertions verify not just what your code returned but how it interacted with its dependencies. That level of verification distinguishes unit testing from just running the code and checking output.
When not to mock
Mocking is powerful, but over-mocking makes tests brittle. Do not mock values like strings or numbers. Do not mock the code you are testing.
Do not mock standard library functions unless they have side effects. A good rule: mock at the boundary of your system.
If it reads a file, mock the open call. If it queries a database, mock the database client. Mock the thing you do not control, not the thing you are testing.
The next article covers measuring test coverage in Python, which shows you how to find code that your tests are not exercising.
Rune AI
Key Insights
- Mock objects replace real dependencies and record every call so you can assert how your code used them.
- Use patch as a decorator or context manager to temporarily replace an object during a test.
- side_effect lets you simulate exceptions, dynamic return values, or a sequence of responses.
- return_value sets what the mock returns when called; the default is a new Mock instance.
- Always patch the name where the object is looked up in the module under test, not where it is defined.
Frequently Asked Questions
When should I use a mock instead of the real object?
What is the difference between Mock and MagicMock?
How does patch know where to apply the mock?
Conclusion
Mock objects let you test your code in isolation by replacing slow, unpredictable, or unavailable dependencies. The Mock class records calls, patch swaps objects temporarily, and side_effect simulates behavior. Together they turn integration problems into unit tests.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.