Skip to main content

Checks PyPI Python 3.11+ License: MIT

declarative-mocks

Declare mock behavior as ordered expectations - readable tests with explicit call patterns, return sequences, and side effects, built on unittest.mock.

Features


  • Fluent expectations - register calls with arguments (positional vs keyword aware), matchers (Anything, ANY_ARGS / ANY_KWARGS, …), and outcomes in order.
  • Return sequences - chain multiple .returns() / .raises() / .runs() for successive matching calls.
  • Call-count controls - one quantifier per expect(...) (.once(), .times(n), .between(min, max), .never(), and more).
  • Spec-backed mocks - aligned with Mock(spec=…) semantics for safer, clearer tests.
  • Async without ceremony - expect() on async def methods is the same DSL; runs() accepts both sync and async callables.
  • No extra runtime dependencies - only the standard library at runtime.

Installation


pip install declarative-mocks

The idea: before and after


Before - unittest.mock: behavior is spread across return_value, side_effect, and ad hoc assertions; ordering and “what happens on the third call?” are easy to lose in a long test.

from unittest.mock import Mock

service = Mock(spec=MyService)
service.fetch_user.side_effect = [
    {"id": 1, "name": "Ada"},
    {"id": 1, "name": "Ada"},
    ConnectionError("retry"),
]
# … exercise code …
# Assertions about call order and counts are often manual and verbose.

After - dmock: the same story is declared next to the mock: ordered expectations, explicit outcomes, and a single verification step.

from dmock import DeclarativeMock

service = DeclarativeMock(MyService)
service.expect("fetch_user").returns({"id": 1, "name": "Ada"}).returns({"id": 1, "name": "Ada"})
service.expect("fetch_user").raises(ConnectionError("retry"))
# … exercise code …
service.verify()

How it compares


The goals are the same as above: a strictly typed expectation DSL, a whitelist mock you construct, call order as a graph, and async wrapping every method in AsyncMock yourself. Against mockito-python and flexmock that looks like this.

Static analysis

That only matters if you type-check tests. Tests deserve the same quality bar as production code - maybe more, now that agents write so much of the suite.

mockito-python is strong on features, but the installed package has no py.typed marker and no .pyi stubs. Then mypy tests fails on import mockito (missing library stubs or py.typed marker) unless you set ignore_missing_imports = True. That is issue #58: the reporter was already ignoring the package; the maintainer called full typing “some significant work.” The ignore makes mypy succeed by treating mockito as Any, so when / thenReturn / verify themselves are not checked.

flexmock is typed (py.typed since 0.11.3), but it addresses methods by string (should_receive("fetch_user")), so the name is still not checked statically.

dmock is the only one of the three that is strictly typed (py.typed) and uses real attribute names on the mock (mock.fetch_user(...)), validated against the spec at construction/expect() time.

Shorter setup and verify

With dmock you write the call once. verify() does not repeat it:

mock.expect("fetch_user", 1).returns(user).once()
# … exercise code …
mock.verify()

mockito splits configuration and verification into two expressions that repeat the call:

when(service).fetch_user(1).thenReturn(user)
# … exercise code …
verify(service, times=1).fetch_user(1)

flexmock uses a four-link chain and a string method name:

flexmock(service).should_receive("fetch_user").with_args(1).and_return(user).once()

A whitelist mock you construct

DeclarativeMock(MyService) is an object you hold and pass in. Unregistered calls fail immediately. Nothing on live modules or instances is rewritten.

mockito's usual when(obj) does rewrite methods on obj (module, class, or instance). The docs say you must unstub() afterwards, or use with / a pytest fixture, or stubs leak into later tests. mock() can create an injectable dummy instead, but it still sits in the same global registry.

flexmock also replaces attributes on existing objects (should_receive). Restore is automatic under pytest and unittest - there is no unstub() you call by hand.

Call order as a dependency graph

.not_before(...) and in_order(...) are checked at the violating call, not after the fact at verify(). Dependencies may cross mock instances. Cycles are rejected at configuration time with ConfigurationError.

Async without caveats

This argument holds against flexmock, not against mockito.

flexmock still has no first-class async API (no async / await / coroutine in the changelog or API reference). The documented workaround is to return an AsyncMock or asyncio.Future yourself.

mockito added first-class async/await stubbing in 2.0.0. Their remaining caveat: introspection metadata such as inspect.iscoroutinefunction on stub wrappers is implemented only on Python 3.12+.

dmock installs a real nested async def dispatcher, so inspect.iscoroutinefunction(mock.aprocess_order) is true on every supported Python (3.11-3.14).

Usage examples


1. Nested return values (successive .returns())

Each .returns() applies to the next matching call in order - handy for paginated or multi-step flows without a hand-built side_effect list.

from dmock import DeclarativeMock

api = DeclarativeMock(ApiClient)
api.expect("next_page").returns({"items": [1], "cursor": "a"})
api.expect("next_page").returns({"items": [2], "cursor": None})

# first call → first dict; second call → second dict

2. Outcome sequences with .runs() and .returns()

Chained outcomes apply to successive matching calls, not to a single call. .runs(fn).returns(value) means: first call runs fn (its return value is the result); second call returns value. A quantifier (.once(), .at_least(n), …) applies to the whole expect(...) chain, not to the last outcome. A second quantifier on the same chain raises ConfigurationError.

If you omit a quantifier, the count is exactly the number of chained outcomes (minimum 1). .runs(fn) alone is one call; a second match is unexpected. The last outcome repeats only when the quantifier allows more calls than outcomes - for example .runs(fn).at_least(1) to run the callable on every matching call.

from dmock import DeclarativeMock

calc = DeclarativeMock(Calculator)
calc.expect("price", 100).runs(lambda x: x - 1).returns(99)

assert calc.price(100) == 99   # from the lambda
assert calc.price(100) == 99   # from .returns(99)

3. Strict argument matching (positional vs keyword)

expect("method") with no extra args means “called with no arguments” - not “anything.” Use matchers when you need to accept arbitrary values.

from dmock import Anything, DeclarativeMock

m = DeclarativeMock(Worker)

# any single positional arg
m.expect("run", Anything()).returns(0)

# keyword-specific expectation
m.expect("run", job_id=123).returns("queued")

4. Variadic arguments (ANY_ARGS, ANY_KWARGS)

To match any positional and keyword arguments (including none), use the dedicated wildcards instead of repeating Anything().

from dmock import ANY_ARGS, ANY_KWARGS, DeclarativeMock

m = DeclarativeMock(Worker)
m.expect("run", ANY_ARGS, ANY_KWARGS).returns(0)

5. Async methods

expect() on an async def spec method needs no extra type. Await the call as usual. runs() accepts a sync callable or an async def; both work.

from dmock import Anything, DeclarativeMock

mock = DeclarativeMock(MyService)
mock.expect("aprocess_order", Anything()).returns("ok")
assert await mock.aprocess_order(1) == "ok"

mock.expect("aprocess_order", Anything()).runs(lambda order_id: f"sync-{order_id}")
assert await mock.aprocess_order(7) == "sync-7"

async def async_fn(order_id: int) -> str:
    return f"async-{order_id}"

mock.expect("aprocess_order", Anything()).runs(async_fn)
assert await mock.aprocess_order(9) == "async-9"

For full DSL details and edge cases, see SPEC.md and REFERENCE.md.

Limitations & non-goals


  • No signature binding. expect("method", …) checks that method exists on the spec, not that the argument list matches the real signature.
  • No automatic verify(). You call verify() yourself (a pytest hook is on the roadmap).
  • No spies and no patching. The library does not wrap live objects, modules, or sys.modules. Construct a DeclarativeMock and pass it in.
  • Not thread-safe. Concurrent use of one mock from multiple threads is out of scope.

Roadmap


After 0.1.0, in no particular order:

  • pytest auto-verify() - a plugin or fixture that runs verify() at the end of each test (gomock-style Finish on teardown), so you do not have to remember the call.
  • Per-outcome counts (maybe) - repeat a .returns() / .raises() / .runs() without chaining it n times (today .returns(1).returns(1).returns(1).returns(2)).
  • Rich matchers - more than Anything / AnythingOfType / MatchedBy: regex, numeric bounds, datetime (and ISO strings), containers, truthy/falsy, plus and / or / not. In the spirit of anys.

Other ideas belong in GitHub Issues.

Development


poetry install
ruff check .
ruff format --check .
mypy src tests
pytest
pytest --cov --cov-report=term-missing

See CONTRIBUTING.md for setup, commits, and tests. Agents follow AGENTS.md.

License

MIT

Metadata

Release files for declarative-mocks 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for declarative-mocks 0.1.1
File Size Uploaded Provenance
declarative_mocks-0.1.1.tar.gz 19.1 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for declarative-mocks 0.1.1
File Interpreter ABI Platform
declarative_mocks-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 36.9 kB

Release files / declarative_mocks-0.1.1.tar.gz

Download URL declarative_mocks-0.1.1.tar.gz
Size 19.1 kB
Tags Source
SHA-256 checksum
How to use checksums
fa497d706d647ceaf5dbeaf8b44b3bb55e373901d1b231bf18b51fea347449a5
BLAKE2b-256 checksum
How to use checksums
eee6fc2b2745a29142135445fde20f2c77efeced6d257e6a215a8472b583031a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Release files / declarative_mocks-0.1.1-py3-none-any.whl

Download URL declarative_mocks-0.1.1-py3-none-any.whl
Size 17.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a0706a6025e144814cfc3bdc7acf86f307ba651e13b74c89dbbfd8b5edec4b65
BLAKE2b-256 checksum
How to use checksums
b369d005b8d7cd8d82b70697265ed0de9fc45e5a84f0ae8925ed65813659d220
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

Security for declarative-mocks 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for declarative_mocks-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page