Search code examples
pythonmockingpytestmagicmock

magic mock set value for attribute only get the first value


I am using pytest3.7 to test. I would like to mock res,which is return value from get_res_function. res.property1[key1][keyN].property2 is the value I want to mock. Here is my test:

@mock.patch("mypkg.get_res_function")
def test_function(mock_res):
    returner = mock.MagicMock()
    returner.property1["key1"]["key2"].property2 = "11111"
    returner.property1["key1"]["key3"].property2 = "22222"
    mock_res.return_value = returner

However, both values i want to mock only use the first line mock's value, which means mock_res.property1["key1"]["key2"].property2 = "11111", mock_res_property1["key1"]["key3"].property2 = "11111" and if reverse it in test code, meaning put "22222" before "11111",

returner.property1["key1"]["key2"].property2 = "22222"
        returner.property1["key1"]["key3"].property2 = "11111"

then all the results is "22222", what is wrong?


Solution

  • This is tricky because the item access syntax thing[index] is actually calling the magic method __getitem__, and that's what you want to mock.

    So this

    returner.property1["key1"]["key2"].property2 = "11111"
    

    Is like doing this

    returner.property1.__getitem__.return_value.__getitem__.return_value.property2 = "11111"
    

    Which is ok, since it's a bit more concise, however the keys are actually ignored.

    You can use a PropertyMock with side_effect to return different things in order:

    type(returner.property1.__getitem__.return_value.__getitem__.return_value).property2 = PropertyMock(side_effect=["11111", "22222"])
    

    Or, back to get item syntax:

    type(returner.property1["this is"]["ignored"]).property2 = PropertyMock(side_effect=["11111", "22222"])
    

    And it should work:

    returner.property1["foo"]["bar"].property2
    > '11111'
    returner.property1["foo"]["bar"].property2
    > '22222'
    

    However, keep in mind that it will return the same values regardless of the input keys. And it will raise StopIteration if it runs out of side_effects.

    You can assert which keys are actually used to call __getitem__:

    returner.property1.__getitem__.call_args_list
    returner.property1.__getitem__.return_value.__getitem__.call_args_list
    

    If you prefer, you could pass a function as side_effect instead, it would be called with the same arguments as the mock (the key) and then you could decide what to return depending on the key. But this would need to be done in the __getitem__ mock, not the property mock.

    Some references: