A fixture that enforces correct mock.patch autospec behaviour, surfacing signature violations that the standard mock library silently ignores.
- Github repository: https://github.com/claudiubelu/mockey/
- Documentation: https://claudiubelu.github.io/mockey/
The standard unittest.mock library has long-standing bugs that let mocked methods be called with the
wrong number or names of arguments without raising a TypeError. Tests pass, but they are not testing
anything meaningful, the real code would raise immediately if called the same way.
There are multiple root causes, some of which have been reported in upstream issues:
- mock#393:
mock.Mockandmock.MagicMockhave noautospec=parameter; usingspec=only checks attribute existence, not call signatures. - mock#396:
mock.patchwithautospec=Truedoes not consume the implicitself/clsargument on instance methods when recording calls, somocked.assert_called_with(a, b)fails unless the assertion also includesself/cls. This friction is part of why people turnautospecoff rather than fight it.
Most of these come down to one root cause: vanilla mock.Mock / mock.MagicMock have no
autospec= parameter at all (mock#393);
spec= only checks that an attribute exists, never that it's called correctly. Everything below
is a different symptom of that same gap, of mock.patch requiring you to remember to opt in to
autospeccing on every single call site, or a genuine feature vanilla unittest.mock lacks
entirely, even via mock.create_autospec.
No autospec= on Mock/MagicMock (mock#393) - every one of these is the mock's own call, or
an attribute access on it, going unchecked:
| Issue | Without mockey | With mockey |
|---|---|---|
mock.Mock(autospec=MyClass).some_method(wrong_args) |
silently accepted | TypeError raised |
mock.Mock(autospec=MyClass).nonexistent_attr |
silently created | AttributeError raised |
mock.Mock(autospec=MyClass)(wrong_args) - the mock standing in for the constructor call itself |
silently accepted | TypeError raised |
mock.Mock(autospec=some_function_or_bound_method)(wrong_args) - the mock's own call, not a class |
silently accepted | TypeError raised |
mock.AsyncMock(autospec=MyClass) |
autospec treated as an arbitrary kwarg |
Same autospec support as Mock / MagicMock: signature enforcement, isinstance, return-type autospeccing |
mock.patch autospec ergonomics - related in spirit to
mock#396, still reproducible on current
Python:
| Issue | Without mockey | With mockey |
|---|---|---|
mock.patch.* with no explicit autospec= passed |
no signature checking at all | autospec=True enforced by default, nothing to opt into per call site |
mock.patch.object(Cls, "method", autospec=True) on an instance method - recorded call args |
include self, so mocked.assert_called_with(a, b) fails and must awkwardly include self |
self / cls excluded from recorded calls, matching how the real method is actually invoked |
Confirmed upstream bugs in functools.partial / functools.partialmethod autospeccing (i.e.
these reproduce with plain mock.patch(..., autospec=True)):
| Issue | Without mockey | With mockey |
|---|---|---|
Patching a functools.partial attribute (module-level or instance) with autospec=True |
zero signature enforcement - any call succeeds | effective (post-binding) signature enforced |
Patching a functools.partialmethod attribute with autospec=True |
resolves to an uncallable NonCallableMagicMock - even a correct call raises TypeError |
effective signature enforced, mock stays callable |
Return-type autospeccing - a genuine gap in vanilla unittest.mock as a whole, not just in
Mock/MagicMock: even mock.create_autospec itself doesn't do this (confirmed directly against
stock stdlib), so there's no existing stdlib mechanism to fall back to here:
| Issue | Without mockey | With mockey |
|---|---|---|
Return value of a method declaring -> SomeClass |
plain MagicMock, no attribute/signature enforcement on it |
autospecced as SomeClass - chained calls are checked too |
Return value of a method declaring -> None |
a MagicMock object |
real None |
mockey is a fork of
oslotest's mock_fixture.py,
which already fixed the mock#393 / mock#396 issues above. Mockey has since added a number of
features and bugfixes beyond that fork point - see
Differences from oslotest for the full list.
Mockey's lazy, access-driven autospeccing is significantly faster than mock.create_autospec,
especially on deep class hierarchies where only a few methods are actually touched per test - see
Performance for the full benchmarks.
A few edge cases mockey doesn't yet handle cleanly - patching builtins on a module, autospeccing
against a custom __setattr__, plus one general mock.patch.dict gotcha unrelated to mockey - are
documented in Known limitations.
pip install mockeypatch_mock_module() must be called before any test module is imported. The reason is that
@mock.patch decorators (including mock.patch.object and mock.patch.multiple) capture
mock._patch at class definition time, not at call time. If patch_mock_module() is called
after the test class is imported, those decorators will use the original, unfixed mock._patch
and signature enforcement will silently not apply.
The canonical place is your test package's __init__.py:
# tests/__init__.py
from mockey.fixture import patch_mock_module
patch_mock_module()This file is imported by Python before any test module in the tests/ package, so all
@mock.patch decorators in all test files pick up the patched version automatically.
Activate MockAutospecFixture in your test's setUp. With testtools:
from mockey import MockAutospecFixture
import testtools
class MyTestCase(testtools.TestCase):
def setUp(self):
super().setUp()
self.useFixture(MockAutospecFixture())With plain unittest:
from mockey import MockAutospecFixture
import unittest
class MyTestCase(unittest.TestCase):
def setUp(self):
super().setUp()
self._fixture = MockAutospecFixture()
self._fixture.setUp()
self.addCleanup(self._fixture.cleanUp)Once the fixture is active, pass autospec= directly to mock.Mock or mock.MagicMock:
from unittest import mock
from mymodule import MyService, MyModel
# Autospec from a class - attribute access and call signatures are enforced.
m = mock.Mock(autospec=MyService)
# Correct call - passes.
m.do_something(user_id=42)
# Wrong signature - raises TypeError, just like the real class would.
m.do_something(unknown_kwarg="oops") # TypeError
# Non-existent attribute - raises AttributeError.
m.typo_metod # AttributeError
# Autospec from an instance works the same way.
service = MyService()
m2 = mock.Mock(autospec=service)
# The mock satisfies isinstance checks against the spec class...
assert isinstance(m, MyService)
# ...and autospeccing a plain callable (function, bound/class/static method)
# enforces its call signature too, not just class constructors.
m3 = mock.Mock(autospec=MyService.do_something)
m3(unknown_kwarg="oops") # TypeErrorIf a method declares a concrete return type, calling it on an autospecced mock returns an autospecced instance of that type - no extra setup required:
class Repository:
def get_user(self, user_id: int) -> User:
...
m = mock.Mock(autospec=Repository)
user_mock = m().get_user(1)
# user_mock is autospecced as User - wrong attribute access raises AttributeError.
user_mock.nonexistent_field # AttributeError
# Methods on user_mock also enforce signatures.
user_mock.update(name="Alice") # passes if that matches User.update's signatureMethods returning None behave correctly too:
class Writer:
def flush(self) -> None:
...
m = mock.Mock(autospec=Writer)
result = m().flush()
assert result is NoneAutospec discovers attributes via dir(), so instance attributes only ever assigned in
__init__ are invisible to it and raise AttributeError. Passing an explicit
spec=[...] / spec_set=[...] list alongside autospec= whitelists extra names:
class Stand:
def __init__(self, speed, durability):
self.speed = speed
self.durability = durability
def punch(self, target):
...
# `speed` / `durability` aren't visible to dir(Stand); whitelist them explicitly.
m = mock.Mock(autospec=Stand, spec_set=["speed", "durability"])
m.speed = 100 # allowed: whitelisted
m.punch("dio") # still signature-checked: autospec's own methods are untouched
m.punch() # TypeError: missing `target`
m.stand_name = "star_platinum" # AttributeError: not on Stand, not whitelistedspec_set=[...] additionally forbids setting attributes outside the combined set (autospec's
own attributes plus the whitelist). This has no clean upstream equivalent: mock.create_autospec's
own spec_set is a bare bool (not a list), and there is no way to widen it for extra names once
spec_set=True is in effect.
With patch_mock_module() active, autospec=True is the default for all patches - you do not
need to write it yourself, or update your existing unit tests:
# Both of these enforce signature checking on Foo.bar.
with mock.patch.object(Foo, "bar"):
...
@mock.patch.object(Foo, "bar")
def test_something(self, mock_bar):
...To opt out of autospeccing for a specific patch, pass autospec=False explicitly:
with mock.patch.object(Foo, "bar", autospec=False):
Foo().bar() # no signature checkingPassing new=, new_callable=, create=, or spec= also disables auto-injection, matching
the standard library's semantics.
See CONTRIBUTING.md for how to set up the development environment, run the
linter (make check), and run the test suite (make test).