Resource library

QA How-To

pytest-mock Tutorial: Mocking with the mocker Fixture

Pytest mock tutorial with runnable mocker fixture examples for patching, spies, stubs, async calls, autospec, failure paths, and test isolation in Python.

20 min read | 3,105 words

TL;DR

Install pytest-mock, request the mocker fixture, and patch dependencies where the module under test looks them up. Assert outputs and calls, use side_effect for errors, and rely on per-test cleanup to prevent leakage.

Key Takeaways

  • Patch the name your code looks up, such as orders.send_receipt.
  • Assert business results and collaborator calls when both are part of the contract.
  • Use side_effect to test failures and check that forbidden downstream work did not run.
  • Apply autospec=True to catch invalid calls at stable method boundaries.
  • Use a spy for safe real behavior and a stub to record callback events.
  • Check await_count for async dependencies and let fixture teardown restore patches.

This Pytest Mock Tutorial shows how to use the mocker fixture to test an order workflow without charging a card or sending an email. You will patch two external boundaries, assert their calls, simulate a decline, observe a real function with a spy, capture callback events, and check an asynchronous lookup. Every code block builds on the same two files and has a command that verifies the step.

The point of a mock is to control a dependency while the code you care about still runs. Keep validation and ordering real. Replace only the payment gateway, receipt sender, or status provider when a test needs to control one of them. pytest-mock cleans up its patches after each function-scoped test, so a passing case cannot quietly leave a patched name behind for the next case.

TL;DR

Test need Fixture call Evidence to assert
Replace a function mocker.patch("orders.send_receipt") Call arguments or no call
Replace one instance method mocker.patch.object(gateway, "charge", autospec=True) Valid signature and exact charge
Raise an error side_effect=PaymentDeclined(...) Exception and suppressed receipt
Watch a real helper mocker.spy(orders, "normalize_email") Call and real returned value
Record a callback mocker.stub() Ordered callback events
Replace an async function mocker.patch("orders.fetch_payment_status") Await count and arguments

The pytest-mock usage reference documents these calls. The underlying call checks and side_effect behavior come from Python's unittest.mock library.

What You Will Build

  • A small orders.py module with validation, payment, receipt, email normalization, and async status functions.
  • A tests/test_orders.py suite that runs entirely offline.
  • Success and failure tests that verify both returned data and effects at service boundaries.
  • A spy and stub example that tests real normalization and callback order.
  • A reusable fixture with two parameterized amounts, giving nine test cases in the final run.

The payment and email functions below raise NotImplementedError because this tutorial does not connect a real provider. That is intentional: an unpatched boundary fails visibly. In a production codebase, patch the adapter your application already uses rather than copying this sample module into your app.

Prerequisites

Use Python 3.12, pytest 9.1.1, and pytest-mock 3.16.0 for the exact commands here. The package releases are recorded on pytest's PyPI page and pytest-mock's PyPI page. If you already work in a project with a lockfile, match its installed versions instead of changing the project to these tutorial pins. Python includes unittest.mock; no extra mock package is necessary.

mkdir pytest-mock-orders
cd pytest-mock-orders
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install 'pytest==9.1.1' 'pytest-mock==3.16.0'
python --version
python -m pytest --version
python -m pip show pytest-mock

On Windows, activate with .venv\\Scripts\\activate in Command Prompt or .venv\\Scripts\\Activate.ps1 in PowerShell. Run python -m pytest --fixtures -q and find mocker in the output. The plugin registers that fixture automatically; tests request it by parameter name. Use python -m pytest rather than a bare pytest command if multiple Python installations exist, because that keeps the runner in the active environment.

Step 1: Create the Order Module

Create orders.py in the new directory. The workflow validates the amount and a sender setting, charges a gateway, sends a receipt to a normalized address, and returns an ID. Its optional callback reports two stages. A separate async wrapper translates a provider status into a Boolean.

# orders.py
import os


class PaymentDeclined(Exception):
    pass


class PaymentGateway:
    def charge(self, customer_id: str, amount_cents: int) -> str:
        raise NotImplementedError("Connect a payment provider here")


def normalize_email(value: str) -> str:
    return value.strip().lower()


def send_receipt(email: str, payment_id: str) -> None:
    raise NotImplementedError("Connect an email provider here")


def place_order(
    customer_id: str,
    email: str,
    amount_cents: int,
    gateway: PaymentGateway,
    on_status=None,
) -> dict[str, object]:
    if amount_cents <= 0:
        raise ValueError("amount_cents must be positive")
    if not os.environ.get("RECEIPT_SENDER"):
        raise RuntimeError("RECEIPT_SENDER is required")
    if on_status is not None:
        on_status("charging")
    payment_id = gateway.charge(customer_id, amount_cents)
    send_receipt(normalize_email(email), payment_id)
    if on_status is not None:
        on_status("paid")
    return {"customer_id": customer_id, "payment_id": payment_id}


async def fetch_payment_status(payment_id: str) -> str:
    raise NotImplementedError("Connect a status provider here")


async def is_payment_complete(payment_id: str) -> bool:
    return await fetch_payment_status(payment_id) == "paid"

Notice the control flow before writing mocks. Invalid amounts fail before configuration is read. A declined charge interrupts execution before the receipt. A successful charge uses the real email normalizer unless a test explicitly replaces it. These facts give later tests meaningful outcomes to protect; the suite is not just testing that mocks can count calls. Keep amounts in integer cents so assertions do not involve floating-point rounding.

Verify: run python -m py_compile orders.py, then python -c "import orders; print(orders.normalize_email(' A@EXAMPLE.COM '))". Compilation should exit silently and the print command should show a@example.com. If import fails, confirm that you are in the directory containing orders.py.

Step 2: Write a Baseline Validation Test

Run mkdir -p tests, then create tests/test_orders.py. Start with the amount guard, which needs no mock. If a later refactor attempts to charge first, the unimplemented gateway will fail this test before ValueError, exposing a change in behavior.

# tests/test_orders.py
import pytest
import orders


def test_rejects_nonpositive_amount():
    gateway = orders.PaymentGateway()
    with pytest.raises(ValueError, match="must be positive"):
        orders.place_order("cust-7", "a@example.com", 0, gateway)

Do not set RECEIPT_SENDER or replace PaymentGateway.charge here. The guard should make those dependencies irrelevant. A common mistake is to prepare every dependency before every test; that can hide the fact that validation has moved after a side effect. This test is also a useful diagnostic baseline: if it fails while every mock-based case passes, investigate control flow before investigating the plugin.

Verify: run python -m pytest tests/test_orders.py::test_rejects_nonpositive_amount -q. Expect one passed test. Keep adding the following functions to this same file. Run commands from the project root so Python can import orders as a top-level module.

Step 3: Patch the Charge and Receipt

Append a success test. mocker.patch.dict supplies the sender setting and restores the mapping after the test. mocker.patch.object replaces charge on this one gateway instance, while mocker.patch replaces the name orders.send_receipt read by place_order.

def test_places_order_and_sends_receipt(mocker):
    gateway = orders.PaymentGateway()
    mocker.patch.dict(orders.os.environ, {"RECEIPT_SENDER": "qa@example.com"})
    charge = mocker.patch.object(
        gateway, "charge", autospec=True, return_value="pay-42"
    )
    receipt = mocker.patch("orders.send_receipt")

    result = orders.place_order(
        "cust-7", " A@EXAMPLE.COM ", 2500, gateway
    )

    assert result == {"customer_id": "cust-7", "payment_id": "pay-42"}
    charge.assert_called_once_with("cust-7", 2500)
    receipt.assert_called_once_with("a@example.com", "pay-42")

The return assertion covers the product result. The two call assertions cover the contract with the dependencies: the customer, exact cents, normalized address, and payment ID. autospec=True makes this patched bound method follow its real signature. It catches accidental extra or misspelled arguments that a general mock would accept. It does not establish that a real processor will accept the charge request; maintain an integration test for that separate question.

Verify: run python -m pytest tests/test_orders.py::test_places_order_and_sends_receipt -q, then python -m pytest tests/test_orders.py -q. The focused run should pass once; the file run should report two passes. The earlier validation test still needs no sender setting because the patch is local to this test.

Step 4: Simulate a Decline and Protect the Failure Path

For a rejected charge, configure side_effect with a PaymentDeclined instance. The test should prove both that the expected exception reaches the caller and that the receipt sender never runs. The latter is a business rule that an exception-only test would miss.

def test_decline_does_not_send_receipt(mocker):
    gateway = orders.PaymentGateway()
    mocker.patch.dict(orders.os.environ, {"RECEIPT_SENDER": "qa@example.com"})
    charge = mocker.patch.object(
        gateway, "charge", autospec=True,
        side_effect=orders.PaymentDeclined("card rejected"),
    )
    receipt = mocker.patch("orders.send_receipt")

    with pytest.raises(orders.PaymentDeclined, match="card rejected"):
        orders.place_order("cust-7", "a@example.com", 2500, gateway)

    charge.assert_called_once_with("cust-7", 2500)
    receipt.assert_not_called()

side_effect may also be a callable that chooses an outcome from arguments, or an iterable that supplies successive outcomes. An exception instance fits this one-attempt case and keeps the expected path clear. The test deliberately asserts that charge did run. assert_not_called belongs on the forbidden downstream action, not on every dependency in the workflow. If this test fails after a refactor, inspect whether the receipt moved into an unconditional cleanup path.

Verify: run python -m pytest tests/test_orders.py::test_decline_does_not_send_receipt -q. Expect one pass. A full file run now reports three passes. Do not change the production exception handler merely to satisfy a mock: first decide whether the intended contract is to propagate or translate provider declines.

Step 5: Patch the Name Used at the Call Site

Patching changes a specific name. place_order resolves send_receipt in the orders module, so mocker.patch("orders.send_receipt") works. Imagine that orders.py instead said from notifications import send_receipt. Once imported, orders.send_receipt is its own reference. Patching notifications.send_receipt later would not necessarily replace that reference. Python's where to patch guide describes this lookup rule.

Add a narrow wiring test by replacing the name orders.normalize_email. This checks that place_order uses the helper's result when it calls the receipt sender.

def test_uses_email_normalizer_result(mocker):
    gateway = orders.PaymentGateway()
    mocker.patch.dict(orders.os.environ, {"RECEIPT_SENDER": "qa@example.com"})
    mocker.patch.object(gateway, "charge", return_value="pay-43")
    normalizer = mocker.patch(
        "orders.normalize_email", return_value="verified@example.com"
    )
    receipt = mocker.patch("orders.send_receipt")

    orders.place_order("cust-8", "  RAW@EXAMPLE.COM  ", 100, gateway)

    normalizer.assert_called_once_with("  RAW@EXAMPLE.COM  ")
    receipt.assert_called_once_with("verified@example.com", "pay-43")

A patch against orders.normalize_email is valid because that is the name place_order evaluates. Avoid many such internal patches in ordinary unit tests: replacing every helper can make tests match an implementation layout rather than a behavior. Here, one helper patch makes the namespace rule visible and establishes that the normalized value is forwarded to the sender.

Verify: run python -m pytest tests/test_orders.py::test_uses_email_normalizer_result -q. This lookup test should pass once. If the normalizer has zero calls, inspect the exact production reference and make sure the patch was installed before invoking place_order.

Step 6: Spy on Real Normalization and Stub the Callback

A spy records a call while running the original function. A stub is an accepting callable whose calls you can inspect. Use both in a test that leaves email normalization real and captures the workflow's status events. Keep the receipt sender patched because a spy there would execute its real side effect or its deliberate NotImplementedError.

def test_real_normalizer_and_status_events(mocker):
    gateway = orders.PaymentGateway()
    mocker.patch.dict(orders.os.environ, {"RECEIPT_SENDER": "qa@example.com"})
    mocker.patch.object(gateway, "charge", return_value="pay-44")
    mocker.patch("orders.send_receipt")
    normalizer_spy = mocker.spy(orders, "normalize_email")
    status = mocker.stub(name="order_status")

    result = orders.place_order(
        "cust-9", " B@EXAMPLE.COM ", 500, gateway, on_status=status
    )

    assert result["payment_id"] == "pay-44"
    normalizer_spy.assert_called_once_with(" B@EXAMPLE.COM ")
    assert normalizer_spy.spy_return == "b@example.com"
    assert status.call_args_list == [
        mocker.call("charging"), mocker.call("paid")
    ]

The ordered call_args_list detects a status sequence of paid before charging, which separate assert_any_call checks would allow. spy_return is a pytest-mock spy property for the last result. If normalization raised, the spy could also expose the last exception through spy_exception. Do not use a spy when the original function contacts a provider; it is an observation tool, not an isolation tool.

Verify: run python -m pytest tests/test_orders.py::test_real_normalizer_and_status_events -q. The spy and callback test should pass once. If spy_return is None, confirm the real function returns a string and that the spy is installed on orders.normalize_email before the action.

Step 7: Check Autospec and Restore One Patch Early

A plain mock accepts almost any argument list. Autospeccing the method rejects a call that differs from its signature. mocker.stop then removes that one patch within the test, allowing you to check that the original method is back. You normally let teardown do this automatically; early restoration is useful only when the before-and-after distinction is the scenario.

def test_autospec_and_early_restore(mocker):
    gateway = orders.PaymentGateway()
    charge = mocker.patch.object(
        gateway, "charge", autospec=True, return_value="pay-45"
    )

    assert gateway.charge("cust-10", 75) == "pay-45"
    charge.assert_called_once_with("cust-10", 75)
    with pytest.raises(TypeError):
        gateway.charge("cust-10", 75, "extra")

    mocker.stop(charge)
    with pytest.raises(NotImplementedError, match="payment provider"):
        gateway.charge("cust-10", 75)

The extra argument is tested on gateway.charge, the patched bound method. The final call reaches PaymentGateway.charge and proves the patch was removed. mocker.stopall() would restore every patch made through this fixture, which is broader than necessary here.

Verify: run python -m pytest tests/test_orders.py::test_autospec_and_early_restore -q. The signature and restoration checks should pass once. If the bad call does not raise TypeError, check that autospec=True is on the patch.object call rather than on a later assertion.

Step 8: Mock an Async Status Function

fetch_payment_status is an async def function. On current Python, patching an async function yields an async-capable mock that records awaits. This example uses asyncio.run inside a normal synchronous pytest test, avoiding an additional async pytest plugin. It invokes the wrapper twice to test the pending and paid outcomes.

def test_async_status_transitions(mocker):
    import asyncio

    status = mocker.patch(
        "orders.fetch_payment_status", side_effect=["pending", "paid"]
    )

    first = asyncio.run(orders.is_payment_complete("pay-46"))
    second = asyncio.run(orders.is_payment_complete("pay-46"))

    assert first is False
    assert second is True
    assert status.await_count == 2
    assert status.await_args_list == [
        mocker.call("pay-46"), mocker.call("pay-46")
    ]

The two values in side_effect are consumed on successive awaits. A regular call_count check would show that a coroutine was created, but it would not prove the caller awaited it. await_count and await_args_list protect the actual async contract. If your suite already has an active event loop through an async test plugin, follow that plugin's convention instead of nesting asyncio.run. Python describes these assertions in the AsyncMock API reference.

Verify: run python -m pytest tests/test_orders.py::test_async_status_transitions -q. The async test should pass once with no unawaited-coroutine warning. If you get a warning, find the coroutine call that was never awaited rather than suppressing the warning.

Step 9: Share Setup Across Parameterized Cases

When several tests need the same boundaries, a fixture can prepare them once per test case and return the mocks for local assertions. Add this import and fixture after the existing imports in tests/test_orders.py. Leave the validation test independent of the fixture so it still catches reordered validation.

# Add after the imports in tests/test_orders.py
from pytest_mock import MockerFixture


@pytest.fixture
def prepared_order(mocker: MockerFixture):
    gateway = orders.PaymentGateway()
    mocker.patch.dict(orders.os.environ, {"RECEIPT_SENDER": "qa@example.com"})
    charge = mocker.patch.object(
        gateway, "charge", autospec=True, return_value="pay-fixture"
    )
    receipt = mocker.patch("orders.send_receipt")
    return gateway, charge, receipt

Append the parameterized test. Pytest creates separate invocations for the one-cent and 2,500-cent amounts; each invocation gets fresh mock call history. The test itself still says what result and boundary calls matter.

@pytest.mark.parametrize("amount_cents", [1, 2500])
def test_valid_amounts_use_exact_cents(prepared_order, amount_cents):
    gateway, charge, receipt = prepared_order

    result = orders.place_order(
        "cust-fixture", "qa@example.com", amount_cents, gateway
    )

    assert result["payment_id"] == "pay-fixture"
    charge.assert_called_once_with("cust-fixture", amount_cents)
    receipt.assert_called_once_with("qa@example.com", "pay-fixture")

Keep setup in the fixture and scenario-specific assertions in the test. A fixture that performs every assertion can obscure which behavior a failed case protects. Move prepared_order into tests/conftest.py only if multiple test modules need it. The pytest fixture guide covers discovery and scope; the type annotation comes from the pytest-mock typing guide.

Verify: run python -m pytest tests/test_orders.py::test_valid_amounts_use_exact_cents -q and expect two passed cases. Then run python -m pytest -q and expect nine passed cases: seven earlier test functions plus two parameterized invocations. If your count differs, use python -m pytest --collect-only -q to see exactly what pytest discovered.

Pytest Mock Tutorial: Choose a Test Double Deliberately

A patch, spy, stub, fake, and integration environment answer different questions. Patch an external collaborator when you need its return value or an error under tight control. Spy on a safe real helper when both the real transformation and its call history matter. Stub a callback that merely accepts events. Write a small in-memory fake when tests need coherent state across several operations, such as recording multiple charges. Use an integration test against the provider's supported test environment when you need to prove its request and response contract.

Choice Good use Limit
mocker.patch Isolate a module-level call Wrong lookup target misses the call
mocker.patch.object Replace one object's method Class-level patches can affect more instances
mocker.spy Observe a deterministic helper Real code and side effects still run
mocker.stub Capture callback arguments No domain behavior is modeled
monkeypatch Change env vars, mappings, paths No built-in mock call record
In-memory fake Model a small stateful dependency May drift from the real provider

Pytest's built-in monkeypatch fixture is especially clear for setting an environment variable. mocker.patch.dict did that here because the tests already requested mocker. Neither choice is universally superior. Decide from the evidence required by the assertion. If a test only needs the sender setting, monkeypatch.setenv is enough. If it must check a receipt call, use a mock or a recorder that exposes the call.

Pytest Mock Tutorial: Diagnose Failed Assertions

When assert_called_once_with fails, inspect the actual arguments before changing expectations. An amount mismatch may be a cents conversion defect; an email mismatch may show that normalization was bypassed. A zero call count can mean the wrong patch target, but it can also mean validation returned early. Trace the control flow from the input and identify the first boundary reached. Treat the assertion diff as data about the production path.

mocker.resetall() clears call history on mocks created by the fixture, while mocker.stopall() restores patched targets. Those operations are not interchangeable. Most tests need neither because fresh function-scoped fixture state already isolates them. If a long test has setup calls that should not count, reset_mock() on one returned mock can establish a new measurement point. Place that reset immediately before the action under test and explain why earlier calls are intentionally ignored.

Do not assert every implementation call. A useful mock test names a behavior that must survive refactoring. The decline test protects the absence of a receipt after a failed payment; the status test protects the order of emitted events. Local helper layout can change while those contracts remain stable. Excessive call checks turn harmless internal edits into test failures and make the suite harder to maintain.

Troubleshooting

  • Problem: fixture 'mocker' not found -> Check python -m pip show pytest-mock and python -m pytest --fixtures -q in the same activated environment. Install the plugin there if missing. Request mocker as a test parameter; do not import a variable named mocker.
  • Problem: the real receipt sender executes despite the patch -> Patch orders.send_receipt, the reference used by place_order. Read the import and call site. Patching another module's original definition may leave an already imported name unchanged.
  • Problem: a mock accepts the wrong number of arguments -> Use autospec=True when patching a stable callable. Check whether the target is an instance method or class method, since binding affects its signature.
  • Problem: async test emits an unawaited-coroutine warning -> Await the wrapper through asyncio.run in this synchronous example, or follow your suite's async plugin. Verify await_count, not just ordinary calls.
  • Problem: call history includes setup activity -> Prefer a separate test for the action. If phases must remain together, reset only the relevant mock immediately before the measured phase. Do not stop all patches just to clear calls.
  • Problem: a spy unexpectedly contacts an external system -> Spies execute original code. Replace service boundaries with patches and reserve spies for safe helpers such as normalize_email.

Interview Questions and Answers

The interviewQnA field below provides model answers on patch targets, autospec, cleanup, declines, spies, callbacks, and async awaits. In an interview, start with the dependency boundary, name the observation you need, and explain what a mocked unit test cannot establish about the live provider. That distinction is more useful than listing mock methods from memory.

Common Mistakes

  • Patching the original definition while the code under test uses a different imported name.
  • Mocking place_order itself, leaving no production decision logic under test.
  • Spying on payment or email code and unintentionally running the real operation.
  • Checking only an exception without confirming forbidden downstream work did not happen.
  • Using an unconstrained mock at a stable interface where autospec could catch signature drift.
  • Sharing a mutable mock across tests and then compensating with broad resets.
  • Treating a mocked unit test as proof that an external provider still accepts requests.

Review the patches whenever a dependency changes. Remove a replacement if no assertion depends on it. Keep a separate integration check for each provider contract your application relies on, because a mock can only enforce the behavior you configured.

Where To Go Next

Run the complete suite from a fresh environment, then apply the same pattern to one external boundary in your own code. Read the pytest tutorial for beginners for broader test structure and pytest versus unittest for framework differences. The pytest-bdd tutorial adds behavior scenarios on top of pytest. The Python API automation framework guide and mocking third-party APIs in tests show how isolated unit checks relate to service-level tests.

Keep the boundary clear: mocked unit tests give fast feedback about your own control flow, while integration tests check the real provider contract. A good test says which name was replaced, why it was replaced, and which business outcome must still hold.

Conclusion

The mocker fixture brings patching, call assertions, spies, and stubs into pytest's fixture lifecycle. This Pytest Mock Tutorial used those tools to protect amount validation, payment and receipt calls, decline behavior, callback order, restoration, and awaited async status checks. Keep patches narrow and choose assertions that express the behavior a refactor must preserve.

Start with one test that currently reaches a network or email boundary. Patch the name your code resolves, run that focused test, then keep an independent integration check for the actual connection.

Interview Questions and Answers

Where should you patch an imported dependency?

Patch the name the module under test resolves at call time. If `orders` imported a sender into its own namespace, I patch `orders.send_receipt` even when another module defines it. I check the import statement and call site first. Patching the definition module alone may leave the local binding unchanged.

How does pytest-mock stop patches leaking between tests?

The function-scoped `mocker` fixture tracks its patches and stops them at teardown. Each test invocation gets fresh fixture state and call history. I avoid module-level mock instances that retain mutable data outside that lifecycle.

Why use autospec on a gateway method?

It applies the method's actual signature to the mock, so an extra argument or misspelled keyword fails. That catches interface drift early. I still assert the charged cents and returned result, because a valid signature does not prove correct values.

How would you test a declined charge?

I give the charge mock a `PaymentDeclined` side effect and assert that the workflow propagates that exception. I verify the attempted charge arguments and that the receipt sender was not called. The latter protects against a success message after failure.

How do a spy and a stub differ?

A spy runs the original function while recording its calls and result. A stub accepts calls without implementing domain behavior, which suits an injected status callback. I would not spy on a live payment or email operation because it would still run.

Why check await_count rather than call_count for an async dependency?

Calling an async function can create a coroutine without consuming it. `call_count` alone cannot prove the caller awaited the dependency. `await_count` and `await_args_list` verify actual awaiting and its arguments.

When would you call mocker.stop?

I use it when one test deliberately needs to check behavior after a temporary replacement is removed. It restores that single target before test teardown. For ordinary tests, automatic fixture cleanup is simpler and less error-prone.

Frequently Asked Questions

What is the mocker fixture in pytest?

It is the fixture registered by the pytest-mock plugin. It wraps unittest.mock patching and adds helpers such as spy and stub. Function-scoped patches made through it are undone after the test.

How do I install pytest-mock?

Install it in the same Python environment as pytest with `python -m pip install pytest-mock`. This tutorial pins a verified release in Prerequisites. Confirm that the fixture is registered with `python -m pytest --fixtures -q`.

What is the difference between mocker.patch and monkeypatch?

Both temporarily change a target. `mocker.patch` returns a mock with call assertions and configurable side effects. Pytest's `monkeypatch` is convenient for environment variables, mappings, paths, and simple attribute replacement.

Why did my mock not affect the function under test?

The patched name may differ from the name the tested module resolves. If `orders.py` calls its own `send_receipt` reference, patch `orders.send_receipt`. Inspect import style and the call site before changing return values.

What does autospec=True check?

It constrains a patched callable to the real target's signature, so wrong argument counts or keyword names fail. It does not prove that argument values are semantically correct or that a provider behaves as the mock does. Add explicit outcome and call assertions.

How do I mock an async function with pytest-mock?

Patch the `async def` function, configure its return value or side effect, and await the code under test. Check `await_count` or `assert_awaited_once_with`, since an ordinary call can create a coroutine without awaiting it. A synchronous test can use `asyncio.run` when no loop is active.

When should I use a spy?

Use a spy when the original function should execute and you need its call record or returned value. Avoid spies on email, payment, and other unwanted side effects. Patch those operations instead.

Related Guides