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.

Four moves from real class to locked double A four-stage sequence: autospec the class so signatures bind, assign the property value directly on the instance mock, add a PropertyMock to the type when read access must be observed, and finally pass spec_set so unknown attribute names are rejected. Four moves from real class to locked double autospec signatures bind assign value plain attribute PropertyMock only if reads matter spec_set unknown names raise Stage 3 costs a class-level patch, so skip it unless access itself is the assertion.
Work left to right and stop at the earliest stage that satisfies the test: most property doubles never need stage three.
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.

How autospec attributes and PropertyMock resolve differently during attribute lookup Two attribute-lookup paths for instance.balance. On the left, create_autospec stores balance as a plain child mock in the instance dict; the type has no data descriptor, so the read falls through and resolves in the instance dict, and the real getter never runs. On the right, a PropertyMock bound to type(instance) is a data descriptor on the class, so Python resolves it there before consulting the instance dict, firing the mock's __get__ and tracking access. A footer states the lookup order: data descriptor on the type wins over the instance dict, which wins over non-data descriptors and class attributes. Reading instance.balance: two resolution paths create_autospec(Account) property collapsed into a plain attribute 1. type(instance) balance: no data descriptor here skip falls through 2. instance.__dict__ balance → child mock (= 250) resolves instance.balance = 250 writes this slot the real getter never runs access is not tracked PropertyMock on type(instance) descriptor reinstated on the class 1. type(instance) balance = PropertyMock (data descriptor) resolves never reached 2. instance.__dict__ not consulted for balance read fires PropertyMock.__get__ access is observed and can raise assert_called_once() works Python attribute lookup order data descriptor on type > instance.__dict__ > non-data descriptor / class attr
Where each patch lands: PropertyMock replaces the descriptor on the class, while an autospecced instance attribute keeps the real class contract and rejects names the class never declared.

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.
  • Slots-based classes hide attributes from autospec. A class using __slots__ exposes slot descriptors rather than instance attributes, so create_autospec records each slot as a plain child mock and spec_set rejects any name outside the slot tuple. Declare the slot explicitly in the test rather than reaching for setattr.
  • Inherited properties resolve through the MRO. PropertyMock must be attached to the class that actually defines the descriptor, or to the concrete subclass; patching a base class leaks the double into every sibling subclass for the duration of the test.
  • 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.

Verifying the double still matches the class

An autospec double is only as honest as the class it was built from, and nothing warns you when the two drift apart in a later refactor. Three checks catch that drift before it reaches CI.

The first is structural: assert against the spec itself rather than against remembered names. mock._spec_class holds the class the double was built from, so a guard test can pin the contract in one line, and dir(instance) returns exactly the attribute surface autospec captured — a name that disappeared from Account disappears from the double on the next run, which turns a rename into a failing test instead of a silently passing one.

Python
from unittest.mock import create_autospec
from account import Account

def test_double_tracks_the_class():
    instance = create_autospec(Account, spec_set=True, instance=True)
    assert instance._spec_class is Account          # the double is bound to this class
    assert "balance" in dir(instance)               # the property still exists
    assert "is_overdrawn" in dir(instance)          # ... and so does the derived one

The second is behavioural: call the double the way production calls it and let the signature binding do the work. create_autospec copies each method's __signature__, so a keyword the real method no longer accepts raises TypeError at call time inside the test — a failure mode a hand-rolled Mock() can never produce, because a bare mock accepts every call shape you throw at it.

The third is a review habit rather than an assertion: prefer instance=True when the code under test receives an instance, not the class. create_autospec(Account) returns a callable class mock whose return_value is the instance double; passing that class mock where an instance is expected means every attribute read resolves on the wrong object and the test asserts nothing useful. Making the instance explicit removes that whole category of false green.

Run the same three checks against every double that stands in for a class you own. The table below shows which guarantee each configuration actually buys you — the gap between plain spec and spec_set is exactly where attribute typos survive.

What each configuration actually enforces A four-column comparison of plain Mock, spec, spec_set and PropertyMock across four guarantees: rejecting unknown attribute reads, rejecting unknown attribute writes, enforcing method signatures, and observing property access. What each configuration actually enforces Criterion Mock() spec spec_set PropertyMock Unknown attribute read raises no yes yes n/a Unknown attribute write raises no no yes n/a Method signature enforced no yes yes n/a Property read observable no no no yes
Only spec_set closes the write path, and only PropertyMock makes a read assertable — the two are complementary, not alternatives.

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.

Can I autospec a dataclass with computed properties? Yes. create_autospec reads the dataclass through the same descriptor walk, so declared fields become writable attribute slots and a @property defined on the dataclass collapses the same way. Field defaults are not reproduced, so assign every value the test depends on explicitly.

← Back to Autospec & Strict Mocking