Mocking

Kenny Chou · July 22, 2025

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

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()