Not sure why MyMock.env["key1"].search.side_effect=["a", "b"] works but MyMock.env["key1"] = ["a"] with MyMock.env["key2"] = ["b"] does not work

Viewed 23

I had created a simple example to illustrate my issue. First is the setup say mydummy.py:

class TstObj:
    def __init__(self, name):
        self.name = name

    def search(self):
        return self.name

MyData = {}
MyData["object1"] = TstObj("object1")
MyData["object2"] = TstObj("object2")
MyData["object3"] = TstObj("object3")

def getObject1Data():
    return MyData["object1"].search()

def getObject2Data():
    return MyData["object2"].search()

def getObject3Data():
    return MyData["object3"].search()

def getExample():
    res = f"{getObject1Data()}{getObject2Data()}{getObject3Data()}"
    return res

Here is the test that failed.

def test_get_dummy1():
    dummy.MyData = MagicMock()
    mydummy.MyData["object1"].search.side_effect = ["obj1"]
    mydummy.MyData["object2"].search.side_effect = ["obj2"]
    mydummy.MyData["object3"].search.side_effect = ["obj3"]

    assert mydummy.getExample() == "obj1obj2obj3"

The above failed with run time error: /Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/unittest/mock.py:1078: StopIteration

Here is the test that passed:

def test_get_dummy2():
    dummy.MyData = MagicMock()
    mydummy.MyData["object1"].search.side_effect = ["obj1", "obj2", "obj3"]

    assert mydummy.getExample() == "obj1obj2obj3"

Am I missing something? I would have expected test_get_dummy1() to work and test_get_dummy2() to fail and not vice versa. Where and how can I find/learn more information about mocking to explain what is going on...

1 Answers

MyData["object1"] is converted to this function call: MyData.__getitem__("object1"). When you call your getExample method, the __getitem__ method is called 3 times with 3 parameters ("object1", "object2", "object3").

To mock the behavior you could have written your test like so:

def test_get_dummy_alternative():
    mydummy.MyData = MagicMock()
    mydummy.MyData.__getitem__.return_value.search.side_effect = ["obj1", "obj2", "obj3"]

    assert mydummy.getExample() == "obj1obj2obj3"

Note the small change from your version: mydummy.MyData["object1"]... became: mydummy.MyData.__getitem__.return_value.... This is the regular MagicMock syntax - we want to to change the return value of the __getitem__ method.

BONUS:

I often struggle with mock syntax and understanding what's happening under the hood. This is why I wrote a helper library: the pytest-mock-generator. It can show you the actual calls made to the mock object.

To use it in your case you could have added this "exploration test":

def test_get_dummy_explore(mg):
    mydummy.MyData = MagicMock()
    mydummy.getExample()
    mg.generate_asserts(mydummy.MyData, name='mydummy.MyData')

When you execute this test, the following output is printed to the console, which contains all the asserts to the actual calls to the mock:

from mock import call

mydummy.MyData.__getitem__.assert_has_calls(calls=[call('object1'),call('object2'),call('object3'),])
mydummy.MyData.__getitem__.return_value.search.assert_has_calls(calls=[call(),call(),call(),])
mydummy.MyData.__getitem__.return_value.search.return_value.__str__.assert_has_calls(calls=[call(),call(),call(),])

You can easily derive from here what has to be mocked.

Related