Skip to content

Repository files navigation

mockey

Release Build status codecov Commit activity License

A fixture that enforces correct mock.patch autospec behaviour, surfacing signature violations that the standard mock library silently ignores.


Background and motivation

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.Mock and mock.MagicMock have no autospec= parameter; using spec= only checks attribute existence, not call signatures.
  • mock#396: mock.patch with autospec=True does not consume the implicit self / cls argument on instance methods when recording calls, so mocked.assert_called_with(a, b) fails unless the assertion also includes self / cls. This friction is part of why people turn autospec off rather than fight it.

What this library offers

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

Relation to oslotest

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.

Performance

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.

Known limitations

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.


Installation

pip install mockey

Usage

Critical: import order

patch_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.

MockAutospecFixture

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)

Using mock.Mock(autospec=...)

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")   # TypeError

Return-value autospeccing

If 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 signature

Methods returning None behave correctly too:

class Writer:
    def flush(self) -> None:
        ...

m = mock.Mock(autospec=Writer)
result = m().flush()
assert result is None

Whitelisting extra attributes with spec= / spec_set=

Autospec 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 whitelisted

spec_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.

Using mock.patch (decorator and context manager)

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 checking

Passing new=, new_callable=, create=, or spec= also disables auto-injection, matching the standard library's semantics.


Contributing

See CONTRIBUTING.md for how to set up the development environment, run the linter (make check), and run the test suite (make test).

About

A fixture that enforces correct `mock.patch` autospec behaviour, surfacing signature violations that the standard mock library silently ignores.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages