mockey
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/
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.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.
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.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
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).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mockey-0.2.0.tar.gz.
File metadata
- Download URL: mockey-0.2.0.tar.gz
- Upload date:
- Size: 71.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43cedb4b8de4005fb6707e4665d4f576558a92da1186da85f8e097477a44ebab
|
|
| MD5 |
7a33444c68349d0515a4c415e76b152d
|
|
| BLAKE2b-256 |
bff52549e7eb5ac307d43bc71c3a355b6552351db69ca8f5e704f8b8e2b39b91
|
File details
Details for the file mockey-0.2.0-py3-none-any.whl.
File metadata
- Download URL: mockey-0.2.0-py3-none-any.whl
- Upload date:
- Size: 13.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd87edb6620fc5744b806a233cb804e5570ff091385e1ea21b584f065e3f06ba
|
|
| MD5 |
44d6e6d9ef7a23954dd4c9eca7d209ee
|
|
| BLAKE2b-256 |
1fcbd366e2d710e031501f34d557d22fe6e42c1a9783703811efbf0faf351310
|