pythonpython-unittest.mockmagicmock

Unexpected failed assert when changing order of python mocks when mocking function that imports other mocked function


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!


Solution

  • 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:

    1. Unittest patches module1.func1b first, since decorators are applied from the bottom up.
      1a. While doing this, unittest internally imports module1.
      1b. When 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.
    2. Unittest now patches 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.
    3. The test executes. When 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").