Home > Software design >  Unit testing that a method was called in another object's method in Python
Unit testing that a method was called in another object's method in Python

Time:11-09

Most of my unit testing experience is with Java and now I'm turning to Python. I need to test whether a method (from object B) gets called inside another method (in object A).

In Java the test method would have to pass a mock or spy version of B to A's constructor to be used when the B method is invoked. Do I need to do the same in Python? Or is there a simpler way? (I raise the possibility of the latter, because it seems, from what little I know, that Python is relatively relaxed about enforcing isolation between different components.)

Below is how I do this the "Java way." There are two Python files under test (for objects A and B) and a test program. Notice that object A's constructor had to be modified to accommodate testing.

obj_a.py

from obj_b import *

class ObjA:

    def __init__(self, *args):
        if len(args) > 0:
            self.objb = args[0] # for testing
            return
        self.objb = ObjB()

    def methodCallsB(self, x, y):
        return self.objb.add(x, y)

obj_b.py

class ObjB:

    def add(self, x, y):
        return x   y

test.py

import unittest
from unittest.mock import patch, Mock
from obj_a import *
from obj_b import *

class TTest(unittest.TestCase):
    @patch("obj_b.ObjB")
    def test_shouldCallBThroughA(self, mockB):
        # configure mock
        mockB.add = Mock(return_value=137)
        obja = ObjA(mockB)

        # invoke test method
        res = obja.methodCallsB(4, 7)
        print("result: "   str(res))

        # assess results
        self.assertEqual(137, res)
        mockB.add.assert_called_once()
        args = mockB.add.call_args[0] # Python 3.7
        print("args: "   str(args))
        self.assertEqual((4, 7), args)

if __name__ =='__main__':
    unittest.main()

Again, is there a simpler way to test that ObjB::add is called from ObjA?

CodePudding user response:

Apart from the possible problems with the design, mentioned in the comment by @Alex, there is a couple of errors in using the mock.

First, you are mocking the wrong object. As in object_a you do from obj_b import * (which is bad style by the way - only import the objects you need), you need to patch the object reference imported into obj_b, e.g. obj_a.ObjB (see where to patch).

Second, you have to mock the method call on the instance instead of the class, e.g. mock mockB.return_value.add instead of mockB.add.

Your tests actually only work because you are not testing your real function, only your mock. If you do the patching correctly, there is no need to add that test-specific code in __init__.

So, put together, something like this should work:

obj_a.py

class ObjA:

    def __init__(self):
        self.objb = ObjB()
...

test.py

class TTest(unittest.TestCase):
    @patch("obj_a.ObjB")
    def test_shouldCallBThroughA(self, mockB):
        # for convenience, store the mocked method
        mocked_add = mockB.return_value.add
        mocked_add.return_value = 137
        obja = ObjA()

        res = obja.methodCallsB(4, 7)

        self.assertEqual(137, res)
        mocked_add.assert_called_once()
        args = mocked_add.call_args[0]
        self.assertEqual((4, 7), args)
  • Related