I am implementing a test for a function func1a() in module1.py using mocks.
The function func1a() calls:
It appears that the order in which mocks are applied, introduces unexpected effects in my test.
module2.py contains:
def func2a()->None:
return None
module1.py contains:
from module2 import func2a
def func1a()->None:
func2a()
func1b()
return None
def func1b()->None:
return None
The test is implemented this way:
from unittest.mock import MagicMock, Mock, call, patch
import pytest
@patch("module1.func1b")
@patch("module2.func2a")
def test_func1a(
mock_func2a:MagicMock,
mock_func1b:MagicMock,
)->None:
from module1 import func1a
mocks = ( mock_func2a,
mock_func1b,
)
mock_manager = Mock()
for mock in mocks:
mock_manager.attach_mock(mock, "mock_" + mock._extract_mock_name())
expected_calls = [
call.mock_func2a(),
call.mock_func1b(),
]
# When
func1a()
# Then
assert mock_manager.mock_calls == expected_calls
and works perfectly.
However, if I invert the order of the patches and keep the rest of the code identical:
@patch("module2.func2a")
@patch("module1.func1b")
def test_func1a(
mock_func1b:MagicMock,
mock_func2a:MagicMock,
)->None:
I encounter the following assertion error
assert [call.mock_func1b()] == [call.mock_fu...mock_func1b()]
At index 0 diff: call.mock_func1b() != call.mock_func2a()
Right contains 2 more items, first extra item: call.mock_func2b()
I suspect it may be due to the order in which the mocks are applied. In particular, I think this is due to the fact that I am mocking a function that import another function, while also mocking this function simultaneously.
Has anyone run into this issue before and can explain why this might be happening?
Thanks!
Unittest patches work in a way that many people find counter-intuitive and confusing. There's a certain sense to it, but you have to think carefully about what you're doing or you end up with strange behavior.
The docs tell us:
patch() works by (temporarily) changing the object that a name points to with another one. There can be many names pointing to any individual object, so for patching to work you must ensure that you patch the name used by the system under test.
The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined. A couple of examples will help to clarify this.
Imagine we have a project that we want to test with the following structure:
a.py -> Defines SomeClass b.py -> from a import SomeClass -> some_function instantiates SomeClass
Now we want to test some_function but we want to mock out SomeClass using patch(). The problem is that when we import module b, which we will have to do when it imports SomeClass from module a. If we use patch() to mock out a.SomeClass then it will have no effect on our test; module b already has a reference to the real SomeClass and it looks like our patching had no effect.
The key is to patch out SomeClass where it is used (or where it is looked up). In this case some_function will actually look up SomeClass in module b, where we have imported it. The patching should look like:
@patch('b.SomeClass')
However, consider the alternative scenario where instead of from a import SomeClass module b does import a and some_function uses a.SomeClass. Both of these import forms are common. In this case the class we want to patch is being looked up in the module and so we have to patch a.SomeClass instead:
@patch('a.SomeClass')
In your case, this means you should be patching both func1b
and func2a
as members of module1
, since module1
is the module being tested:
@patch("module1.func2a")
@patch("module1.func1b")
def test_func1a(
mock_func1b:MagicMock,
mock_func2a:MagicMock,
)->None:
...
This eliminates the need to patch in a specific order. Conveniently, it also allows you to move the line from module1 import func1a
to the beginning of the file, where it will feel much more at home.
Now, why does your test currently fail when you swap the order of the patches? I'm guessing a little, but I'm pretty sure this is the order of events:
module1.func1b
first, since decorators are applied from the bottom up.module1
.module1
is imported, it executes this line: from module2 import func2a
. Module 1 now has a reference to func2a that has not been patched yet.module2.func2a
. As the documentation says, this patch only replaces the reference to func2a
in the module2
namespace, but does not affect the reference owned by module1
.func1a
is called, it calls the mocked version of func1b
and the original version of func2a
. The test fails.Note that if you refactored module1
to handle imports like this instead:
import module2
def func1a()->None:
module2.func2a()
func1b()
return None
def func1b()->None:
return None
Then you would once again need to use @patch("module2.func2a")
.