简体   繁体   中英

python - mock - class method mocked but not reported as being called

learning python mocks here. I need some helps to understand how the patch work when mocking a class.

In the code below, I mocked a class. the function under tests receives the mock and calls a function on it. In my assertions, the class is successfully called, but the function is reported as not being called.

I added a debug print to view the content in the function under tests and it is reported as called.

My expectation is the assertion assert facadeMock.install.called should be true. Why is it not reported as called and how do I achieve this?

Thank you.

install/__init__.py

from .facade import Facade

def main():
    f = Facade()
    f.install()
    print('jf-debug-> "f.install.called": {value}'.format(
        value=f.install.called))

test/install_tests.py

import os
import sys
# allow import of package
sys.path.insert(0,
                os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from unittest.mock import patch

import install

@patch('install.Facade') # using autospec=True did not change the result
def test_main_with_links_should_call_facade_install_with_link_true(facadeMock):
    install.main()

    assert facadeMock.called
    assert facadeMock.install is install.Facade.install
    assert facadeMock.install.called   # <-------------------- Fails here!

output:

============================= test session starts ==============================
platform linux -- Python 3.10.6, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/jfl/ubuntu-vim, configfile: pytest.ini
collected 1 item                                                               

test/install_tests.py jf-debug-> "f.install.called": True
F

=================================== FAILURES ===================================
________ test_main_with_links_should_call_facade_install_with_link_true ________

facadeMock = <MagicMock name='Facade' id='140679041900864'>

    @patch('install.Facade')
    def test_main_with_links_should_call_facade_install_with_link_true(facadeMock):
        install.main()
    
        assert facadeMock.called
    
        assert facadeMock.install is install.Facade.install
    
>       assert facadeMock.install.called
E       AssertionError: assert False
E        +  where False = <MagicMock name='Facade.install' id='140679042325216'>.called
E        +    where <MagicMock name='Facade.install' id='140679042325216'> = <MagicMock name='Facade' id='140679041900864'>.install

test/install_tests.py:21: AssertionError
=========================== short test summary info ============================
FAILED test/install_tests.py::test_main_with_links_should_call_facade_install_with_link_true - AssertionError: assert False
============================== 1 failed in 0.09s ===============================

[edit]

Thank you to @chepner and @Daniil Fajnberg for their comments. I found the cause of the problem.

The problem can be reduced at: install/__init__.py receives an instance of Facade when calling Facade() in main(). This instance is not the same as the one received in parameters of the test. They are different instances.

to retrieve the instance received in main(), do:

    actualInstance = facadeMock.return_value
    assert actualInstance.install.called

And it works!

Thank you. That really helps me understand the working of mocks in python.

[/edit]

I have found a method to solve your problem; it is empirical but it works.

To pass your test I have modified it as you can see below:

@patch('install.Facade') # using autospec=True did not change the result
def test_main_with_links_should_call_facade_install_with_link_true(facadeMock):
    install.main()

    assert facadeMock.called
    assert facadeMock.install is install.Facade.install

    #assert facadeMock.install.called   # <-------------------- Fails here!
    install_called = False
    for call_elem in facadeMock.mock_calls:
        if call_elem[0] == "().install":
            install_called = True
            break
    assert install_called == True

The mock objects facadeMock and f

facadeMock is a mock object created in the test code and it is used by the production code during your test to create the mock object f by the instruction:

f = Facade()

In the production code f is a mock object (that is an instance of the class Mock ) because it is created by the Mock object Facade that is exactly facadeMock .

But f and facadeMock are 2 different instances of the class Mock .

Below I show the id values of facadeMock , Facade and f :

facadeMock = <MagicMock name='Facade' id='140449467990536'>
Facade = <MagicMock name='Facade' id='140449467990536'>
f = <MagicMock name='Facade()' id='140449465274608'>

The id for facadeMock , Facade but are different from the id of f .

The attribute mock_calls

When your test code is executed the function install.main() execution causes the definition of the attribute mock_calls for the mock object facadeMock .

This attribute is a list of complex elements.
If you check the first field (I mean the field in position 0) of each one of this element you can find the name of the methods of the mock that are called.

In your case you have to found install and to do this you have to look for ().install . So my test checks all the element of mock_calls and only if ().install is found set the variable install_called=True .

I hope that this answer can help you.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM