简体   繁体   中英

side_effect function of PropertyMock gets called only once

I have two questions regarding the mocking of a property with unittest.mock.PropertyMock (code will follow below):

  1. Why does the test test_prop output "text_0, text_0" and not "text_0, text_1" as the test test_func ? The output indicates, that the function side_effect_func in test_prop gets called only once, while I would have expected it to get called twice, like in test_func .
  2. How can I specify a side_effect function for a property, that gets called every time the property is accessed?

My use case is that I would like to have a mock that returns a different name (which is a property) depending on how often it was called. This would "simulate" two different instances of Class1 to Class2 in the following minimal example.

The code:
File dut.py :

class Class1():
    def __init__(self, name):
        self.__name = name

    @property
    def name(self):
        return self.__name

    def name_func(self):
        return self.__name


class Class2():
    def __init__(self, name, class1):
        self.__name = name
        self.__class1 = class1

    @property
    def name(self):
        return self.__name
    @property
    def class1(self):
        return self.__class1

File test\test_dut.py (the second with-statement produces the exact same behavior when swapped with the first one):

import dut
import unittest
from unittest.mock import patch, PropertyMock

class TestClass2(unittest.TestCase):
    def test_func(self):
        side_effect_counter = -1
        def side_effect_func(_):
            nonlocal side_effect_counter
            side_effect_counter += 1
            return f'text_{side_effect_counter}'

        c2_1 = dut.Class2('class2',  dut.Class1('class1'))
        c2_2 = dut.Class2('class2_2', dut.Class1('class1_2'))
        with patch('test_dut.dut.Class1.name_func', side_effect=side_effect_func, autospec=True):
            print(f'{c2_2.class1.name_func()}, {c2_1.class1.name_func()}')

    def test_prop(self):
        side_effect_counter = -1
        def side_effect_func():
            nonlocal side_effect_counter
            side_effect_counter += 1
            return f'text_{side_effect_counter}'

        c2_1 = dut.Class2('class2',  dut.Class1('class1'))
        c2_2 = dut.Class2('class2_2', dut.Class1('class1_2'))
        with patch.object(dut.Class1, 'name', new_callable=PropertyMock(side_effect=side_effect_func)):
        # with patch('test_dut.dut.Class1.name', new_callable=PropertyMock(side_effect=side_effect_func)):
            print(f'{c2_2.class1.name}, {c2_1.class1.name}')

Call from command line: pytest -rP test\test_dut.py This leads to the following output (problematic line marked by me):

============================================================================================== test session starts ==============================================================================================
platform win32 -- Python 3.9.12, pytest-7.1.2, pluggy-1.0.0
rootdir: C:\Users\klosemic\Documents\playground_mocks
plugins: hypothesis-6.46.5, cov-3.0.0, forked-1.4.0, html-3.1.1, metadata-2.0.1, xdist-2.5.0
collected 2 items

test\test_dut.py ..                                                                                                                                                                                        [100%]

==================================================================================================== PASSES =====================================================================================================
_____________________________________________________________________________________________ TestClass2.test_func ______________________________________________________________________________________________
--------------------------------------------------------------------------------------------- Captured stdout call ----------------------------------------------------------------------------------------------
text_0, text_1
_____________________________________________________________________________________________ TestClass2.test_prop ______________________________________________________________________________________________
--------------------------------------------------------------------------------------------- Captured stdout call ----------------------------------------------------------------------------------------------
text_0, text_0 <<<<<< HERE IS THE PROBLEM
=============================================================================================== 2 passed in 0.46s ===============================================================================================

The issue has to do with how you are instantiating the PropertyMock . To answer your first question about why the second test prints test_0 for both calls, you instantiate the PropertyMock class during your with patch.object(get_events.Class1, 'name', new_callable=PropertyMock(side_effect=side_effect_func)) call.

Since you instantiate the class, it gets called immediately at the end of that line due to the logic of the __enter__ method in the patch object. You can see that logic in this line and then this one . Due to that the value of your side_effect immediately becomes a string, which is essentially the output of the first call to the PropertyMock . This can be confirmed by changing your function to the following and observing the output:

with patch.object(dut.Class1, 'name', new_callable=PropertyMock(side_effect=side_effect_func)) as mock_prop:
    # print(f'{c2_2.class1.name}, {c2_1.class1.name}')
    print(mock_prop)

You will notice that this prints text_0 in the console, confirming what has been mentioned above.

To answer your second question, the way to use PropertyMock in this case would be to change the second test to the following:

with patch.object(dut.Class1, 'name', new_callable=PropertyMock) as mock_prop:
    mock_prop.side_effect = side_effect_func
    print(f'{c2_2.class1.name}, {c2_1.class1.name}')

Then when you run the tests you get the correct output as shown below.


============================================================= test session starts =============================================================
platform darwin -- Python 3.8.9, pytest-7.0.1, pluggy-1.0.0
rootdir: ***
plugins: asyncio-0.18.3, mock-3.7.0
asyncio: mode=strict
collected 2 items                                                                                                                             

tests/test_dut.py ..                                                                                                                 [100%]

=================================================================== PASSES ====================================================================
____________________________________________________________ TestClass2.test_func _____________________________________________________________
------------------------------------------------------------ Captured stdout call -------------------------------------------------------------
text_0, text_1
____________________________________________________________ TestClass2.test_prop _____________________________________________________________
------------------------------------------------------------ Captured stdout call -------------------------------------------------------------
text_0, text_1
============================================================== 2 passed in 0.01s ==============================================================

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