pytest-mock

repository·main·Indexed 24 days ago

https://github.com/pytest-dev/pytest-mock

A pytest plugin providing a thin wrapper around the mock package. It introduces the `mocker` fixture (and scope-specific variants like `session_mocker`) to provide a safe way to patch, spy, and stub objects with automatic cleanup. Key features include improved mock call assertion reporting, support for the standalone `mock` PyPI package, and full type annotations via `MockerFixture` for static type checkers like mypy.

Tokens
3.5K
Snippets
9
Records
23
Agent score
84%

What's inside pytest-mock

  1. Overview of pytest-mock features

    main

    The pytest-mock plugin enhances pytest testing workflows by providing the mocker fixture. Key features include:

    • Automatic Cleanup: All mocks created via the mocker fixture are automatically undone after the test completes.
    • Patching API: A thin wrapper around the standard mock library's patching capabilities.
    • Spying and Stubbing: Provides utilities like spy and stub for observing or replacing behavior.
    • Pytest Introspection: Uses pytest introspection when comparing calls to improve error reporting and accuracy.
  2. How improved mock call assertion reporting works

    main

    When a mock call assertion fails, pytest-mock intercepts the AssertionError to provide a cleaner output. It hides internal traceback entries from the mock module and uses pytest's advanced assertions to provide a detailed diff of the arguments. This is particularly useful for identifying differences in complex or nested arguments in Args and Kwargs.

    mocker = <pytest_mock.MockerFixture object at 0x0381E2D0>
    
        def test(mocker):
            m = mocker.Mock()
            m('fo')
        >       m.assert_called_once_with('', bar=4)
        E       AssertionError: Expected call: mock('', bar=4)
        E       Actual call: mock('fo')
        E
        E       pytest introspection follows:
        E
        E       Args:
        E       assert ('fo',) == ('',)
        E         At index 0 diff: 'fo' != ''
        E         Use -v to get the full diff
        E       Kwargs:
        E       assert {} == {'bar': 4}
        E         Right contains more items:
        E         {'bar': 4}
        E         Use -v to get the full diff
  3. Why use pytest-mock instead of standard mock.patch

    main

    While the standard mock library provides patch via context managers, decorators, or contextlib.ExitStack, pytest-mock is designed to solve the scaling and complexity issues associated with them:

    1. Avoids Nesting: Unlike with mock.patch(...) statements, which lead to excessive indentation and broken test flow as the number of patches increases.
    2. Cleaner Function Signatures: Unlike @mock.patch decorators, pytest-mock does not force you to accept mock objects as function parameters. This avoids issues with parameter ordering and prevents conflicts with pytest fixtures or pytest.mark.parametrize.
    3. Easier Lifecycle Management: Unlike decorators, using the mocker fixture allows you to easily manage the lifecycle of mocks within the test execution without complex parameter management or ExitStack boilerplate.
  4. How to use Spies to track existing methods

    main

    A Spy (mocker.spy) allows a method to behave exactly like the original implementation while tracking calls, return values, and exceptions. This is useful when you want the real logic to run but need to verify it was called correctly.

    mocker.spy works with:

    • Normal functions and methods
    • Class and static methods
    • async def functions (as of version 3.0.0)

    Spy Attributes:

    • spy_return: The last value returned by the spied function.
    • spy_return_iter: A duplicate of the last returned value if it was an iterator (requires duplicate_iterators=True in the spy call).
    • spy_return_list: A list of all returned values (available in version 3.13+).
    • spy_exception: The last exception raised, or None if no exception occurred.

    As of version 3.10, you can selectively stop a spy using mocker.stop(spy).

    def test_spy_method(mocker):
        class Foo(object):
            def bar(self, v):
                return v * 2
    
        foo = Foo()
        spy = mocker.spy(foo, 'bar')
        assert foo.bar(21) == 42
    
        spy.assert_called_once_with(21)
        assert spy.spy_return == 42
  5. Use the mocker fixture to patch objects

    main

    The pytest-mock plugin provides a mocker fixture that acts as a thin wrapper around the mock package's patching API. Using the mocker fixture is preferred over manual patching because it automatically undoes all mocks at the end of the test, ensuring test isolation. You can use mocker.patch(target) to replace an object with a mock and then use standard mock assertion methods like assert_called_once_with.

    import os
    
    class UnixFS:
    
        @staticmethod
        def rm(filename):
            os.remove(filename)
    
    def test_unix_fs(mocker):
        mocker.patch('os.remove')
        UnixFS.rm('file')
        os.remove.assert_called_once_with('file')
  6. Use the mocker fixture for patching

    main

    The mocker fixture provides an API identical to unittest.mock.patch. It is used to replace objects in your code with mocks for testing purposes. Because it is a fixture, mocks are automatically undone when the test function finishes, preventing side effects in other tests.

    Supported patching methods include:

    • mocker.patch(target)
    • mocker.patch.object(target, attribute)
    • mocker.patch.multiple(target, *args, **kwargs)
    • mocker.patch.dict(mapping, keys, values)

    You can also use mocker.stopall(), mocker.stop(mock_object), or mocker.resetall() to manage mocks manually.

  7. How mocker manages mock lifecycles

    main

    The mocker fixture manages a MockCache. When you use mocker.patch or mocker.spy, the resulting mock and its associated patcher are registered in this cache.

    Key lifecycle behaviors:

    • Automatic Cleanup: At the end of the fixture's scope (e.g., the end of a test function), mocker.stopall() is called, which iterates through the cache and stops all registered patchers.
    • Manual Control: You can manually stop a specific patch by calling mocker.stop(mock_object).
    • Resetting: You can call mocker.resetall() to call reset_mock() on all mocks managed by the fixture, allowing you to clear call history without removing the patches themselves.
  8. Disable improved mock call assertion reporting

    main

    The plugin monkeypatches the mock library to provide improved error reporting and introspection for mock call assertions (like Mock.assert_called_with()). If you encounter issues with this feature, you can disable it in your pytest.ini file using mock_traceback_monkeypatch = false.

    Note that this feature is automatically disabled when running pytest with the --tb=native option.

    [pytest]
    mock_traceback_monkeypatch = false
  9. Use the standalone 'mock' package instead of unittest.mock

    main

    By default, pytest-mock uses the unittest.mock module bundled with Python. If you want to use the latest version of the mock package from PyPI, you can force the plugin to import the standalone mock module by setting mock_use_standalone_module = true in your pytest.ini configuration.

    [pytest]
    mock_use_standalone_module = true
  10. Use the mocker fixture

    main

    The mocker fixture is the primary entry point for pytest-mock. It provides an interface identical to the standard unittest.mock module but ensures that all patches and spies are automatically undone at the end of the test (or at the end of the scope defined by the fixture).

    Available fixture scopes:

    • mocker: function scope (default)
    • class_mocker: class scope
    • module_mocker: module scope
    • package_mocker: package scope
    • session_mocker: session scope
  11. Avoid using mocker as a context manager or decorator

    main

    The mocker fixture is designed to be used within the body of a test function. Using it as a context manager or a function decorator is not supported and will emit a warning.

    Incorrect usage:

    with mocker.patch.object(a, 'doIt'):
        ...

    If you specifically need to mock a context manager, use mocker.patch.context_manager instead to avoid the warning.

    def test_context_manager(mocker):
        a = A()
        # DO NOT DO THIS: it will emit a warning
        with mocker.patch.object(a, 'doIt', return_value=True, autospec=True):  
            assert a.doIt() == True