A fixture that enforces correct mock.patch autospec behaviour, surfacing signature violations that the standard mock library silently ignores.
Project description
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
This library is based on
oslotest's mock_fixture.py,
extracted and extended as a standalone package.
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 implicitselfargument on instance methods, causing every patched-method call to fail the signature check (so people turnautospecoff).
What this library fixes
| Issue | Without mockey | With mockey |
|---|---|---|
mock.Mock(autospec=MyClass) - calls with wrong args |
silently accepted | TypeError raised |
mock.Mock(autospec=MyClass) - non-existent attribute |
silently created | AttributeError raised |
mock.patch.* - calls with wrong args |
silently accepted | TypeError raised |
mock.patch.* - no explicit autospec=True needed |
must opt in per-patch | enforced globally |
Return value of def get_foo(self) -> Foo (via mock.Mock(autospec=…)) |
plain MagicMock |
autospecced as Foo |
Return value of def get_foo(self) -> Foo (via mock.patch.object(…)) |
plain MagicMock |
autospecced as Foo |
Return value of def get_none(self) -> None |
MagicMock object |
None |
Constructor: mock.Mock(autospec=MyClass)(wrong_args) |
silently accepted | TypeError raised |
| Patching an already-mocked attribute | silently double-patches | InvalidSpecError raised |
What this library adds compared to oslotest
In addition to what oslotest's mock_fixture fixes, this library adds on top of that:
- Return-value autospeccing from type hints: if a method declares
-> SomeClass, its mock return value is automatically autospecced asSomeClass, so chained calls are also checked. -> Noneenforcement: methods annotated-> Nonereturn actualNone, matching runtime behaviour and preventing tests from accidentally asserting on aMagicMockreturn value.- Constructor signature enforcement: calling
mock.Mock(autospec=MyClass)(wrong_args)raisesTypeError, just as calling the real class' constructor would.
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)
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).
Project details
Release history Release notifications | RSS feed
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.1.0.tar.gz.
File metadata
- Download URL: mockey-0.1.0.tar.gz
- Upload date:
- Size: 63.0 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 |
5fefe420436cedeb84c0de593dcbcb113dbdf8c8a7d2a7228aff22d127500c78
|
|
| MD5 |
4093a9ad784433ab7132d9a88bdee021
|
|
| BLAKE2b-256 |
f8e0c87cdc63a500a2cc5f08d8ded93baa82e2ec36c671ddee0ceb2429f12dd5
|
File details
Details for the file mockey-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mockey-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.3 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 |
a73d0c7fbc77ac87689009a314bd07c4a379a7b442cc00d1e0fa807ecf48b211
|
|
| MD5 |
ea7c1925d16578d5516efffcd392aa12
|
|
| BLAKE2b-256 |
53951d960fe4791807ee85a267256cf848bb0bd847f0aa570ea695401b720fc7
|