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:

  1. Execution Passthrough: Any attribute lookup or invocation is dispatched to the underlying object, executing its actual code and returning its real result.
  2. Call Inspection (Spying): The mock captures all invocation metadata, enabling assertions such as assert_called_once_with(), inspection of call_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