Skip to content

Commit cc73ca9

Browse files
committed
Updates the README.md file
Adds information abou the fixture, what it fixes, what it brings in addition to the oslotest's fixture, and how to use it.
1 parent 5ae671d commit cc73ca9

1 file changed

Lines changed: 191 additions & 3 deletions

File tree

README.md

Lines changed: 191 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,199 @@
66
[![Commit activity](https://img.shields.io/github/commit-activity/m/claudiubelu/mockey)](https://img.shields.io/github/commit-activity/m/claudiubelu/mockey)
77
[![License](https://img.shields.io/github/license/claudiubelu/mockey)](https://img.shields.io/github/license/claudiubelu/mockey)
88

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

1111
- **Github repository**: <https://github.com/claudiubelu/mockey/>
12-
- **Documentation** <https://claudiubelu.github.io/mockey/>
12+
- **Documentation**: <https://claudiubelu.github.io/mockey/>
1313

1414
---
1515

16-
Repository initiated with [osprey-oss/cookiecutter-uv](https://github.com/osprey-oss/cookiecutter-uv).
16+
## Background and motivation
17+
18+
This library is based on
19+
[oslotest's `mock_fixture.py`](https://github.com/openstack/oslotest/blob/0b348bccd3639b6d2a8acd19e62b1dd19ad903d6/oslotest/mock_fixture.py),
20+
extracted and extended as a standalone package.
21+
22+
The standard `unittest.mock` library has long-standing bugs that let mocked methods be called with the
23+
wrong number or names of arguments without raising a `TypeError`. Tests pass, but they are not testing
24+
anything meaningful, the real code would raise immediately if called the same way.
25+
26+
There are multiple root causes, some of which have been reported in upstream issues:
27+
28+
- [mock#393](https://github.com/testing-cabal/mock/issues/393): `mock.Mock` and `mock.MagicMock`
29+
have no `autospec=` parameter; using `spec=` only checks attribute *existence*, not call signatures.
30+
- [mock#396](https://github.com/testing-cabal/mock/issues/396): `mock.patch` with `autospec=True`
31+
does not consume the implicit `self` argument on instance methods, causing every patched-method
32+
call to fail the signature check (so people turn `autospec` off).
33+
34+
## What this library fixes
35+
36+
| Issue | Without mockey | With mockey |
37+
|---|---|---|
38+
| `mock.Mock(autospec=MyClass)` - calls with wrong args | silently accepted | `TypeError` raised |
39+
| `mock.Mock(autospec=MyClass)` - non-existent attribute | silently created | `AttributeError` raised |
40+
| `mock.patch.*` - calls with wrong args | silently accepted | `TypeError` raised |
41+
| `mock.patch.*` - no explicit `autospec=True` needed | must opt in per-patch | enforced globally |
42+
| Return value of `def get_foo(self) -> Foo` (via `mock.Mock(autospec=…)`) | plain `MagicMock` | autospecced as `Foo` |
43+
| Return value of `def get_foo(self) -> Foo` (via `mock.patch.object(…)`) | plain `MagicMock` | autospecced as `Foo` |
44+
| Return value of `def get_none(self) -> None` | `MagicMock` object | `None` |
45+
| Constructor: `mock.Mock(autospec=MyClass)(wrong_args)` | silently accepted | `TypeError` raised |
46+
| Patching an already-mocked attribute | silently double-patches | `InvalidSpecError` raised |
47+
48+
## What this library adds compared to oslotest
49+
50+
In addition to what `oslotest`'s `mock_fixture` fixes, this library adds on top of that:
51+
52+
- **Return-value autospeccing from type hints**: if a method declares `-> SomeClass`, its mock
53+
return value is automatically autospecced as `SomeClass`, so chained calls are also checked.
54+
- **`-> None` enforcement**: methods annotated `-> None` return actual `None`, matching runtime
55+
behaviour and preventing tests from accidentally asserting on a `MagicMock` return value.
56+
- **Constructor signature enforcement**: calling `mock.Mock(autospec=MyClass)(wrong_args)` raises
57+
`TypeError`, just as calling the real class' constructor would.
58+
59+
---
60+
61+
## Installation
62+
63+
```bash
64+
pip install mockey
65+
```
66+
67+
## Usage
68+
69+
### Critical: import order
70+
71+
`patch_mock_module()` must be called **before any test module is imported**. The reason is that
72+
`@mock.patch` decorators (including `mock.patch.object` and `mock.patch.multiple`) capture
73+
`mock._patch` at *class definition time*, not at call time. If `patch_mock_module()` is called
74+
after the test class is imported, those decorators will use the original, unfixed `mock._patch`
75+
and signature enforcement will silently not apply.
76+
77+
The canonical place is your test package's `__init__.py`:
78+
79+
```python
80+
# tests/__init__.py
81+
from mockey.fixture import patch_mock_module
82+
83+
patch_mock_module()
84+
```
85+
86+
This file is imported by Python before any test module in the `tests/` package, so all
87+
`@mock.patch` decorators in all test files pick up the patched version automatically.
88+
89+
### MockAutospecFixture
90+
91+
Activate `MockAutospecFixture` in your test's `setUp`. With `testtools`:
92+
93+
```python
94+
from mockey import MockAutospecFixture
95+
import testtools
96+
97+
class MyTestCase(testtools.TestCase):
98+
def setUp(self):
99+
super().setUp()
100+
self.useFixture(MockAutospecFixture())
101+
```
102+
103+
With plain `unittest`:
104+
105+
```python
106+
from mockey import MockAutospecFixture
107+
import unittest
108+
109+
class MyTestCase(unittest.TestCase):
110+
def setUp(self):
111+
super().setUp()
112+
self._fixture = MockAutospecFixture()
113+
self._fixture.setUp()
114+
self.addCleanup(self._fixture.cleanUp)
115+
```
116+
117+
### Using `mock.Mock(autospec=...)`
118+
119+
Once the fixture is active, pass `autospec=` directly to `mock.Mock` or `mock.MagicMock`:
120+
121+
```python
122+
from unittest import mock
123+
from mymodule import MyService, MyModel
124+
125+
# Autospec from a class - attribute access and call signatures are enforced.
126+
m = mock.Mock(autospec=MyService)
127+
128+
# Correct call - passes.
129+
m.do_something(user_id=42)
130+
131+
# Wrong signature - raises TypeError, just like the real class would.
132+
m.do_something(unknown_kwarg="oops") # TypeError
133+
134+
# Non-existent attribute - raises AttributeError.
135+
m.typo_metod # AttributeError
136+
137+
# Autospec from an instance works the same way.
138+
service = MyService()
139+
m2 = mock.Mock(autospec=service)
140+
```
141+
142+
### Return-value autospeccing
143+
144+
If a method declares a concrete return type, calling it on an autospecced mock returns an
145+
autospecced instance of that type - no extra setup required:
146+
147+
```python
148+
class Repository:
149+
def get_user(self, user_id: int) -> User:
150+
...
151+
152+
m = mock.Mock(autospec=Repository)
153+
user_mock = m().get_user(1)
154+
155+
# user_mock is autospecced as User - wrong attribute access raises AttributeError.
156+
user_mock.nonexistent_field # AttributeError
157+
158+
# Methods on user_mock also enforce signatures.
159+
user_mock.update(name="Alice") # passes if that matches User.update's signature
160+
```
161+
162+
Methods returning `None` behave correctly too:
163+
164+
```python
165+
class Writer:
166+
def flush(self) -> None:
167+
...
168+
169+
m = mock.Mock(autospec=Writer)
170+
result = m().flush()
171+
assert result is None
172+
```
173+
174+
### Using `mock.patch` (decorator and context manager)
175+
176+
With `patch_mock_module()` active, `autospec=True` is the default for all patches - you do not
177+
need to write it yourself, or update your existing unit tests:
178+
179+
```python
180+
# Both of these enforce signature checking on Foo.bar.
181+
with mock.patch.object(Foo, "bar"):
182+
...
183+
184+
@mock.patch.object(Foo, "bar")
185+
def test_something(self, mock_bar):
186+
...
187+
```
188+
189+
To opt out of autospeccing for a specific patch, pass `autospec=False` explicitly:
190+
191+
```python
192+
with mock.patch.object(Foo, "bar", autospec=False):
193+
Foo().bar() # no signature checking
194+
```
195+
196+
Passing `new=`, `new_callable=`, `create=`, or `spec=` also disables auto-injection, matching
197+
the standard library's semantics.
198+
199+
---
200+
201+
## Contributing
202+
203+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up the development environment, run the
204+
linter (`make check`), and run the test suite (`make test`).

0 commit comments

Comments
 (0)