mercredi 29 mai 2019

Cannot attach mocks with an autospec

# mod.py

def foo():
    bar1("arg1")
    bar2("arg2")

def bar1(x):
    pass

def bar2(x):
    pass

Suppose we want to test foo, asserting that it calls bar1 and bar2 in that order. It's possible like this:

# test_mod.py

from mod import foo

def test_foo(mocker):
    mock = mocker.MagicMock()
    mock.attach_mock(mocker.patch("mod.bar1"), "b1")
    mock.attach_mock(mocker.patch("mod.bar2"), "b2")
    foo()
    mock.assert_has_calls(
        [
            mocker.call.b1("arg1"),
            mocker.call.b2("arg2"),
        ]
    )

The mocker fixture is from pytest-mock plugin. Execute the MCVE with python -m pytest.

It works, however this approach has a big flaw in practice. Should the signature of bar2 change (e.g. it's not defined directly in mod.py but was imported from a third-party dependency), then foo can become completely broken yet the tests can not detect such a change, because that dependency was mocked out in the first place.

If something upstream changed in bar2, for example another argument is added:

def bar2(x, y):
    pass

The tests still pass, but the library code is now broken:

>>> from mod import foo
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    bar2("arg2")
TypeError: bar2() missing 1 required positional argument: 'y'

The usual solution to this issue is to autospec your mocks. An autospec would cause foo() to fail during the test, too, because bar2 is called with incompatible signature.

Updated library:

def foo():
    bar1("arg1")
    bar2("arg2x", "arg2y")

def bar1(x):
    pass

def bar2(x, y):
    pass

Updated test:

from mod import foo

def test_foo(mocker):
    mock = mocker.MagicMock()
    mock.attach_mock(mocker.patch("mod.bar1"), "b1")
    mock.attach_mock(mocker.patch("mod.bar2", autospec=True), "b2")
    foo()
    mock.assert_has_calls(
        [
            mocker.call.b1("arg1"),
            mocker.call.b2("arg2x", "arg2y"),
        ]
    )

These tests now fail for reasons I don't understand.

E       AssertionError: Calls not found.
E       Expected: [call.b1('arg1'), call.b2('arg2x', 'arg2y')]
E       Actual: [call.b1('arg1')]

Why did adding autospec break the attach_mock feature? How should you assert the order of calls without losing autospec?

Aucun commentaire:

Enregistrer un commentaire