Isolation & Contracts

Mocking Properties and Class Attributes with Autospec

create_autospec is the strictest tool in unittest.mock for binding a test double to a real interface, but engineers are routinely surprised when a @property on the spec class becomes a writable plain attribute on the mock — assigning mock.value = 10 works, yet the original getter never runs and PropertyMock-style access tracking is gone. This happens because autospec inspects the class through the descriptor protocol and freezes each member's evaluated shape, collapsing computed properties into static attribute slots. This guide shows how to mock properties and class attributes correctly: when to set attributes directly on an autospec instance, when PropertyMock is mandatory, and how spec_set locks the surface in both directions.

Prerequisites

  • Python 3.8+ (AsyncMock and the modern create_autospec attribute handling). Examples target 3.11.
  • unittest.mock from the standard library — no third-party packages.
  • Familiarity with the descriptor protocol (__get__/__set__) and with autospec strict mocking fundamentals.

Solution

The core rule: create_autospec records a property as a plain attribute you assign to directly. Use PropertyMock only when the act of accessing the attribute must be observed or must raise.

Python
import unittest.mock as mock
from unittest.mock import create_autospec, PropertyMock, patch


class Account:
    def __init__(self, balance: int) -> None:
        self._balance = balance

    @property
    def balance(self) -> int:               # computed getter on the real class
        return self._balance

    @property
    def is_overdrawn(self) -> bool:         # derived property with logic
        return self._balance < 0

    def withdraw(self, amount: int) -> int:
        self._balance -= amount
        return self._balance


# 1. Autospec the class. Methods are bound to real signatures; unknown
#    attributes raise AttributeError. Properties become plain attributes.
MockAccount = create_autospec(Account, spec_set=True)
instance = MockAccount(balance=100)         # __init__ signature is enforced

# 2. Set the property value DIRECTLY — autospec exposes `balance` as a
#    writable attribute, NOT as a descriptor. The real getter never runs.
instance.balance = 250
assert instance.balance == 250              # plain read of the stored value

# 3. Methods still enforce their signature thanks to autospec.
instance.withdraw(50)                       # OK: matches (self, amount)
try:
    instance.withdraw(1, 2, 3)              # wrong arity -> caught at call time
except TypeError as exc:
    print("signature enforced:", exc)

# 4. When the code under test must OBSERVE property access — or the getter
#    must raise — attach a PropertyMock to the *type*, not the instance.
type(instance).is_overdrawn = PropertyMock(return_value=True)
assert instance.is_overdrawn is True
type(instance).is_overdrawn.assert_called_once()   # access was tracked

# 5. A PropertyMock can raise on read, modelling a getter that errors.
type(instance).is_overdrawn = PropertyMock(side_effect=RuntimeError("stale"))
try:
    _ = instance.is_overdrawn               # access triggers the side effect
except RuntimeError as exc:
    print("getter raised on access:", exc)


# Patching a property on a real (non-autospec) object also requires
# PropertyMock bound to the class, because data descriptors live on the type.
def test_patch_real_property():
    real = Account(balance=100)
    with patch.object(Account, "balance", new_callable=PropertyMock) as m_balance:
        m_balance.return_value = 999
        assert real.balance == 999          # patched getter wins over _balance
        m_balance.assert_called_once_with()

Why this works

Autospec walks the spec object with dir() and getattr, and for a property it reads the descriptor's evaluated value (typically None or a sample), then stores a child mock in a normal attribute slot — the descriptor's __get__/__set__ machinery is discarded. That is why direct assignment works and getter side effects vanish. PropertyMock reinstates descriptor behaviour: it is itself a descriptor, so it must live on the class (type(instance)), where Python's attribute lookup consults data descriptors before the instance __dict__. spec_set=True then forbids creating attributes the real class never declared, turning interface drift into an immediate AttributeError.

Edge cases and failure modes

  • Assigning a PropertyMock to the instance does nothing. instance.prop = PropertyMock(...) just stores the mock object as a value; reading instance.prop returns the PropertyMock itself, never calling it. Always bind to type(instance) or use patch.object(Class, "prop", new_callable=PropertyMock).
  • spec_set rejects derived test-only attributes. If your test wants to stash a helper flag on the mock, spec_set=True raises AttributeError. Use plain spec (via create_autospec(Cls) without spec_set) when you need to attach scratch attributes.
  • Class-level attributes vs instance attributes diverge. Autospec mirrors attributes defined at class scope onto both the class mock and its return_value. A value set only inside __init__ may not appear unless an instance-level sample existed during introspection; assign it explicitly on the instance mock.
  • PropertyMock and assert_called_with arguments. A property getter takes no arguments, so assert with assert_called_once_with() (empty); setters invoked via PropertyMock receive the assigned value as the sole positional argument.
  • Autospec does not deep-spec the property's return type. The value you get back is a generic child mock, not an autospec of the declared return class. If callers chain methods on the returned value, wrap it with another create_autospec. Pairing this with resolving side_effect and return_value conflicts avoids surprises when a getter both returns and raises.

Frequently Asked Questions

Why does create_autospec turn a property into a plain attribute mock? Autospec inspects the class via the descriptor protocol and records the property's evaluated type as a static attribute, not as a descriptor. The resulting mock holds a child mock you assign to directly; it never calls the original getter or fires side effects on access.

Do I need PropertyMock if I use create_autospec on a class? Only when the code under test depends on getter side effects or you must assert the property was accessed. For a value you can set directly, assign instance.attr on the autospec instance. Use PropertyMock when read access itself must be observed or must raise.

What is the difference between spec and spec_set for attributes? spec raises AttributeError when you read or call an attribute the real object lacks but still lets you set arbitrary new attributes. spec_set additionally raises AttributeError when you set an attribute the spec class does not define, locking the surface in both directions.

← Back to Autospec & Strict Mocking