0

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?

JohnK
  • 6,865
  • 8
  • 49
  • 75
  • Why are you creating ObjB in __init__ of ObjA? That is not something you'd normally do in Python (unless there is a very good reason for it). Also, ObjB should not be a class, but just a function. – Alex Nov 03 '22 at 18:20
  • I originally created ObjB in `methodCallsB`, but I moved creation to the constructor because I wanted to pass the mocked ObjB through the constructor instead of through `methodCallsB`: need the latter method to be a simple function for its use in the non-test code. You object that ObjB should be just a function: you do realize that what I've shown here is from much more complicated real-world code that I've simplified for ease of communication, don't you? The omitted parts to the real version of ObjB would complicate things unnecessarily. – JohnK Nov 03 '22 at 20:25
  • JohnK, sidenotes: it is not a constructor, but a initializer -the object was already constructed, that is why init has `self`. (the real constructor in Python is `__new__`) In python, a class is not a placeholder for functions, so it is unusual to have chained things like `self.objb.add()` (and it is slower), especially when the object itself is not used. – Alex Nov 04 '22 at 10:02
  • @Alex, thanks for that clarification! One might also wonder why someone would make simple addition even a separate function. – JohnK Nov 08 '22 at 15:05

1 Answers1

1

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)
MrBean Bremen
  • 14,916
  • 3
  • 26
  • 46