Metadata-Version: 2.4
Name: mockey
Version: 0.3.0
Summary: A fixture that enforces correct mock.patch autospec behaviour, surfacing signature violations that the standard mock library silently ignores.
Project-URL: Homepage, https://claudiubelu.github.io/mockey/
Project-URL: Repository, https://github.com/claudiubelu/mockey
Project-URL: Documentation, https://claudiubelu.github.io/mockey/
Author-email: Claudiu Belu <cbelu@cloudbasesolutions.com>
License-File: LICENSE
Keywords: python
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: <4.0,>=3.10
Requires-Dist: fixtures
Description-Content-Type: text/markdown

# mockey

[![Release](https://img.shields.io/github/v/release/claudiubelu/mockey)](https://img.shields.io/github/v/release/claudiubelu/mockey)
[![Build status](https://img.shields.io/github/actions/workflow/status/claudiubelu/mockey/main.yml?branch=main)](https://github.com/claudiubelu/mockey/actions/workflows/main.yml?query=branch%3Amain)
[![codecov](https://codecov.io/gh/claudiubelu/mockey/branch/main/graph/badge.svg)](https://codecov.io/gh/claudiubelu/mockey)
[![Commit activity](https://img.shields.io/github/commit-activity/m/claudiubelu/mockey)](https://img.shields.io/github/commit-activity/m/claudiubelu/mockey)
[![License](https://img.shields.io/github/license/claudiubelu/mockey)](https://img.shields.io/github/license/claudiubelu/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](https://github.com/testing-cabal/mock/issues/393): `mock.Mock` and `mock.MagicMock`
  have no `autospec=` parameter; using `spec=` only checks attribute *existence*, not call signatures.
- [mock#396](https://github.com/testing-cabal/mock/issues/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](https://github.com/testing-cabal/mock/issues/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](https://github.com/testing-cabal/mock/issues/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`](https://github.com/openstack/oslotest/blob/0b348bccd3639b6d2a8acd19e62b1dd19ad903d6/oslotest/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**](docs/vs-oslotest.md) 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**](docs/performance.md) 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**](docs/known-limitations.md).

---

## Installation

```bash
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`:

```python
# 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`:

```python
from mockey import MockAutospecFixture
import testtools

class MyTestCase(testtools.TestCase):
    def setUp(self):
        super().setUp()
        self.useFixture(MockAutospecFixture())
```

With plain `unittest`:

```python
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`:

```python
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:

```python
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:

```python
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:

```python
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:

```python
# 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:

```python
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](CONTRIBUTING.md) for how to set up the development environment, run the
linter (`make check`), and run the test suite (`make test`).
