File: remarks.rst

package info (click to toggle)
pytest-mock 3.14.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 336 kB
  • sloc: python: 1,464; makefile: 19
file content (102 lines) | stat: -rw-r--r-- 2,884 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
=======
Remarks
=======

Type annotations
----------------

``pytest-mock`` is fully type annotated, letting users use static type checkers to
test their code.

The ``mocker`` fixture returns ``pytest_mock.MockerFixture`` which can be used
to annotate test functions:

.. code-block:: python

    from pytest_mock import MockerFixture

    def test_foo(mocker: MockerFixture) -> None:
        ...

The type annotations have been checked with ``mypy``, which is the only
type checker supported at the moment; other type-checkers might work
but are not currently tested.


Why bother with a plugin?
=========================

There are a number of different ``patch`` usages in the standard ``mock`` API,
but IMHO they don't scale very well when you have more than one or two
patches to apply.

It may lead to an excessive nesting of ``with`` statements, breaking the flow
of the test:

.. code-block:: python

    import mock

    def test_unix_fs():
        with mock.patch('os.remove'):
            UnixFS.rm('file')
            os.remove.assert_called_once_with('file')

            with mock.patch('os.listdir'):
                assert UnixFS.ls('dir') == expected
                # ...

        with mock.patch('shutil.copy'):
            UnixFS.cp('src', 'dst')
            # ...


One can use ``patch`` as a decorator to improve the flow of the test:

.. code-block:: python

    @mock.patch('os.remove')
    @mock.patch('os.listdir')
    @mock.patch('shutil.copy')
    def test_unix_fs(mocked_copy, mocked_listdir, mocked_remove):
        UnixFS.rm('file')
        os.remove.assert_called_once_with('file')

        assert UnixFS.ls('dir') == expected
        # ...

        UnixFS.cp('src', 'dst')
        # ...

But this poses a few disadvantages:

- test functions must receive the mock objects as parameter, even if you don't plan to
  access them directly; also, order depends on the order of the decorated ``patch``
  functions;
- receiving the mocks as parameters doesn't mix nicely with pytest's approach of
  naming fixtures as parameters, or ``pytest.mark.parametrize``;
- you can't easily undo the mocking during the test execution;

An alternative is to use ``contextlib.ExitStack`` to stack the context managers in a single level of indentation
to improve the flow of the test:

.. code-block:: python

    import contextlib
    import mock

    def test_unix_fs():
        with contextlib.ExitStack() as stack:
            stack.enter_context(mock.patch('os.remove'))
            UnixFS.rm('file')
            os.remove.assert_called_once_with('file')

            stack.enter_context(mock.patch('os.listdir'))
            assert UnixFS.ls('dir') == expected
            # ...

            stack.enter_context(mock.patch('shutil.copy'))
            UnixFS.cp('src', 'dst')
            # ...

But this is arguably a little more complex than using ``pytest-mock``.