Mocking is tremendously important in unit testing because it isolates the unit under test. Use mocking to prevent unnecessary calls to external resources and control your object behavior to test for edge cases!
With mocking you achieve:
-
Isolation from external dependencies: Real components like databases, APIs, or file systems are slow or unpredictable. Mocks simulate these so tests are fast and deterministic.
-
Focus on logic: You test only your code’s logic, not whether other systems work correctly.
-
Control behavior: Mocks let you define exactly how dependencies behave, including edge cases and failures.
-
Improve speed and reliability: Since mocks avoid real network/database calls, tests run quickly and reliably.
-
Detect indirect outputs: You can verify that certain methods were called with expected arguments — useful for side effects or integrations.
Let’s see how we can make use of mocking in our tests.
Contents:
- Patching an object
- The MagicMock Object
- Set
.return_value
to specify return value of a mockedmethod()
- Set
.side_effect
for exceptions and dynamic behaviors - Patching multiple objects
- If mocking a class object, use Autospec
- Mock vs MagicMock vs patch
- Other Tips
- Test Script
Patching an object
Here’s a piece of code we want to test. It calls get_database_object()
, which simply returns None
. In production, the object could have multiple methods and attributes, which can take on datatypes like nested dictionaries. Our code’s behavior is dependent upon the values of these attributes and dictionaries:
def get_database_object():
# let's pretend this returns something complicated
return None
def check_object_attributes():
# without Mocking, the obj type is None
obj = get_database_object()
if obj is None:
raise Exception("object is None")
# in production, obj.attr could be None for a varierty of reasons
if obj.attr is None:
raise ValueError('attribute is not set')
# in production, obj.attr is expected to be some data type
if isinstance(obj.attr,dict):
raise TypeError("the attribute is a dict instance")
# obj.method returns some value
if obj.method() == "magical":
raise Exception("a magical string was returned")
Without mocking, calling check_obj_attributes()
raises an Exception:
import pytest
def test_check_object_is_none():
# First, this test illustrates that without mocking, an exception will be raised
with pytest.raises(Exception, match="object is None"):
check_object_attributes()
With mocking, we pretend get_database_object()
returns something that is not None:
from unittest.mock import patch, MagicMock
# Set module.path.get_database_object to the actual location of get_database_object()
@patch('module.path.get_database_object')
def test_check_object_is_not_none(mock_get_db_obj):
# This test patches `get_database_object` with a "magic mock" object
mock_obj = MagicMock()
# Setting the mocked object's return_value controls the returned value of get_database_object()
mock_get_db_obj.return_value = mock_obj
check_object_attributes() # should not raise
In this example, we patch
the returned value of get_database_object()
to be MagicMock()
.
Since the get_database_object()
function now returns a MagicMock
object, it passes the None check.
The MagicMock Object
Interestingly, the other checks passed as well! obj
should not have a .attr
attribute, so how come no exception is raised?
Let’s put a breakpoint()
in the function we’re testing to examine obj
, which you’ll find is a MagicMock
object. Try calling any random attribute or method of obj
, like obj.random_attribute
or obj.method()
or obj.attr.another_attr.method()
. You’ll always get another MagicMock
object back.
The next examples illustrates how we control this MagicMock
object to trigger the exceptions in check_object_attributes()
:
@patch('module.path.get_database_object')
def test_check_object_attributes_none_attr(mock_get_db_obj):
# Manually set the object's .attr to be None
mock_obj = MagicMock()
mock_obj.attr = None
# get_database_object() -> mock_obj, and mock_obj.attr == None
mock_get_db_obj.return_value = mock_obj
# this check passes
with pytest.raises(ValueError, match='attribute is not set'):
check_object_attributes()
@patch('module.path.get_database_object')
def test_check_object_attributes_none_attr(mock_get_db_obj):
# Similarly, manually set the object's .attr to be a dict
mock_obj = MagicMock()
mock_obj.attr = {}
# get_database_object() -> mock_obj, and mock_obj.attr == {}
mock_get_db_obj.return_value = mock_obj
# this check passes
with pytest.raises(TypeError, match='the attribute is a dict instance'):
check_object_attributes()
Set .return_value
to specify return value of a mocked method()
@patch('module.path.get_database_object')
def test_check_object_attributes_missing_key(mock_get_db_obj):
# If mocking a method(), set the method's return_value
mock_obj = MagicMock()
mock_obj.method.return_value = "magical"
# get_database_object -> mock_obj, and mock_obj.method() -> "magical"
mock_get_db_obj.return_value = mock_obj
with pytest.raises(Exception, match="a magical string was returned"):
check_object_attributes()
Set .side_effect
for exceptions and dynamic behaviors
The previous examples sets the values of a mocked object using mock_obj.attr.return_value
or by directly setting mock_obj.attr = some_value
. But if we want to set mock_obj
to raise an error, we’d use .side_effect
:
mock_func.side_effect = ValueError("oops")
mock_func.side_effect = lambda x: x * 2
Patching multiple objects
Maybe your function makes calls to multiple databases. You can mock multiple objects like this:
@patch('module.path.get_database_object1')
@patch('module.path.get_database_object2')
def test_check_object_attributes_missing_key(mock_db_obj2,mock_db_obj1):
mock_db_obj1 = MagicMock()
mock_db_obj2 = MagicMock()
...
The positional arguments correspond to the @patch
‘d objects in the reverse order.
If mocking a class object, use Autospec
Signature Enforcement
Any functions or methods (including constructors) on the mock will have their call signatures checked against the real object’s signature. This means if you try to call a mocked method with incorrect arguments (e.g., wrong number of arguments, or non-existent keyword arguments), it will raise an error, catching potential bugs early.
Attribute Validation
Autospec also ensures that you can only access attributes that actually exist on the real object. Attempting to access a non-existent attribute on an autospecced mock will result in an AttributeError, preventing tests from passing when they shouldn’t.
Examples
Via mock.patch
from unittest.mock import patch
@patch('some_module.MyClass', autospec=True)
def test_my_function(self, MockMyClass):
# MockMyClass behaves like MyClass, enforcing signatures and attributes
instance = MockMyClass()
instance.some_method(arg1, arg2) # Will raise error if signature is wrong
Via create_autospec(class)
from unittest.mock import create_autospec
class MyClass:
def my_method(self, a, b):
pass
mock_instance = create_autospec(MyClass)
mock_instance.my_method(1, 2) # Valid call
# mock_instance.my_method(1) # Will raise TypeError due to missing argument
Mock vs MagicMock vs patch
-
Mock
is a general-purpose object. -
MagicMock
adds support for magic methods (__len__
,__getitem__
, etc). -
patch
temporarily replaces an object for the duration of a test or context.
Other Tips
- It’s always better to use real data for testing if possible. Don’t over-mock
- use
mock.method.assert_called_once()
to ensure the method has been invoked - use
mock.method.assert_called_with(...)
to ensure the method has been invoked with specific args - use
mock.method_calls
to inspect all calls and arguments
Test Script
Putting together some of the examples from above:
from unittest.mock import patch, MagicMock, ANY, Mock
import pytest
def get_database_object():
return None
def check_object_attributes():
obj = get_database_object()
if obj is None:
raise Exception("object is None")
# obj.attr could be None for a varierty of reasons
if obj.attr is None:
raise ValueError('attribute is not set')
# in production, obj.attr is expected to be some data type
if isinstance(obj.attr,dict):
raise TypeError("the attribute is a dict instance")
# obj.method returns some value
if obj.method() == "magical":
raise Exception("a magical string was returned")
# from your_module import check_object_attributes
@patch('kfchou.scratch.get_database_object')
def test_check_object_is_not_none(mock_get_db_obj):
mock_obj = MagicMock()
mock_get_db_obj.return_value = mock_obj
check_object_attributes() # should not raise
def test_check_object_is_none():
with pytest.raises(Exception, match="object is None"):
check_object_attributes()
@patch('kfchou.scratch.get_database_object')
def test_check_object_attributes_none_attr(mock_get_db_obj):
mock_obj = MagicMock()
mock_obj.attr = None
mock_get_db_obj.return_value = mock_obj
with pytest.raises(ValueError, match='attribute is not set'):
check_object_attributes()
@patch('kfchou.scratch.get_database_object')
def test_check_object_attributes_missing_key(mock_get_db_obj):
mock_obj = MagicMock()
mock_obj.method.return_value = "magical"
mock_get_db_obj.return_value = mock_obj
with pytest.raises(Exception, match="a magical string was returned"):
check_object_attributes()
@patch('kfchou.scratch.get_database_object')
def test_check_object_attributes_none_attr(mock_get_db_obj):
# Similarly, manually set the object's .attr to be a dict
mock_obj = MagicMock()
mock_obj.attr = {}
# get_database_object() -> mock_obj, and mock_obj.attr == {}
mock_get_db_obj.return_value = mock_obj
# this check passes
with pytest.raises(TypeError, match='the attribute is a dict instance'):
check_object_attributes()