Using wraps in unittest.mock to Spy on Objects
In Python's unittest.mock library, the
wraps parameter allows developers to implement the "spy"
pattern by wrapping a real callable or object. Unlike a standard mock
that replaces real logic with dummy values, a wrapped mock forwards
calls to the original implementation while simultaneously recording call
counts, arguments, and return values. This article explains what the
wraps argument accomplishes, how it differs from default
mocks, and how to use it effectively in test suites.
The Purpose of the
wraps Argument
When writing unit tests, you often need to verify that a specific
function or method was invoked with expected arguments without altering
its actual execution. Standard mocks (Mock() or
MagicMock()) return another mock instance by default,
completely neutralizing the underlying behavior.
Supplying an existing object or function to the wraps
parameter achieves two main objectives:
- Execution Passthrough: Any attribute lookup or invocation is dispatched to the underlying object, executing its actual code and returning its real result.
- Call Inspection (Spying): The mock captures all
invocation metadata, enabling assertions such as
assert_called_once_with(), inspection ofcall_count, and tracking of call histories.
How wraps Works in
Practice
When you pass an object to wraps, the mock intercepts
calls, delegates them to the wrapped object, and records the
interaction.
from unittest.mock import Mock
class Calculator:
def add(self, a, b):
return a + b
# Instantiate the real object
real_calc = Calculator()
# Create a spy by wrapping the real object
spy_calc = Mock(wraps=real_calc)
# Execute the method
result = spy_calc.add(2, 3)
# 1. The real code executed
assert result == 5
# 2. Call history was recorded
spy_calc.add.assert_called_once_with(2, 3)In this example, spy_calc.add(2, 3) runs the real
add logic, producing 5, while still allowing
standard mock assertions.
Wrapping Functions Directly
The wraps argument works with standalone functions as
well as class instances:
from unittest.mock import Mock
def calculate_tax(amount):
return amount * 0.2
spy_tax = Mock(wraps=calculate_tax)
total = spy_tax(100)
assert total == 20.0
assert spy_tax.call_count == 1
spy_tax.assert_called_with(100)Selective Overriding
A wrapped mock retains the ability to override specific attributes or
methods. If you set a return_value or a
side_effect on a specific attribute of the mock, it takes
precedence over the wrapped implementation.
from unittest.mock import Mock
class Service:
def fetch_data(self):
return "live data"
def process_data(self):
return "processed"
service = Service()
spy_service = Mock(wraps=service)
# Override fetch_data, but leave process_data alone
spy_service.fetch_data.return_value = "mocked data"
assert spy_service.fetch_data() == "mocked data"
assert spy_service.process_data() == "processed"This flexibility makes wraps useful for partial mocking,
where only external boundaries (such as network calls) are silenced,
while internal processing methods run untouched.
Summary of Key Behaviors
- Preserves Real Logic: Ensures business logic, calculations, and internal side effects execute as intended.
- Non-Destructive Monitoring: Provides full access to
mock assertions (
assert_called(),call_args_list, etc.) without writing custom logging or tracking code. - Targeted Overrides: Permits selective replacement of individual methods while retaining passthrough behavior for the rest of the object.