Python – Unit Testing an Impure Function with Method Calls

pythonunit testing

I wish to test the following method:

class MyClass(object):
    ...
    def my_method(self, args):
        ...  # Code which transforms args into arg1, arg2, ..., argN
        self.A.other_method1(arg1, arg2)
        self.A.other_method2(arg1, arg2, arg3)
        self.A.other_method3(arg1)
        self.B.other_method4(arg1, arg2)
        self.B.other_method5(arg1, arg2, arg4)
        self.B.other_method6(arg1, arg2, arg5, arg6)

Obviously this is an artificial example. I have created this example to focus on what I believe are the essential points:

  • my_method first does some straightforward work to transform the values it is given. I am not concerned with how to test this aspect.
  • my_method then calls a sequence of methods on other attributes A and B of the class. I am not concerned with how to test those other methods.
  • The sequence of method calls mutates A and B. So my_method is very much a non-pure function. I don't believe I can redesign to make the function pure, as the other methods all do I/O.

My current approach is to mock out A and B using unittest.mock.MagicMock, and then assert that these methods were called in the required order, with the expected arguments. My test code therefore looks like this:

from unittest.mock import MagicMock, call

class MyTest(unittest.TestCase):
    def test_MyClass_my_method(self):
        x = MyClass()  # Instantiation details omitted
        x.A = MagicMock()
        x.B = MagicMock()
        x.my_method(0)
        result_A = x.A.method_calls
        result_B = x.B.method_calls
        expected_result_A = [call.other_method1(1, 2),
                             call.other_method2(1, 2, 3),
                             call.other_method2(1)]
        expected_result_B = [call.other_method4(1, 2),
                             call.other_method5(1, 2, 4),
                             call.other_method6(1, 2, 5, 6)]

        self.assertEquals(result_A, expected_result_A)
        self.assertEquals(result_B, expected_result_B)

This works. But I can't help feel I have simply rewritten my_method – and, worse, tightly coupled the test to the inner working of the method.

Is there a better way to test this method? Is the method actually poorly designed?

Best Answer

This indeed seems to be an improvable design. Here are a few things that I'd consider looking at:

  • You said you cannot make it pure due to I/O. But separating the calculation of what is to be output and the actual output allows you to keep most of it pure.

  • The test doesn't make any sense. It does not verify anything useful, but as you mentioned, it only tightly couples to the inner working. Again, this begs the question on what it is the whole method should verifiably do?

  • Output testing seems to be missing somewhere around here. You may have tested these other_methods and what they output, but testing a method call order is still different from testing the actual output resulting from them. You hope that the composition of these works correctly, but that's really what higher-order integration tests are for. Given an integration test that verifies the whole output created by my_method there seems to be little benefit left for the test in question.

  • Finally, A and B might be worth to look at as well. Given that the method call order is so important, I would consider looking at these methods and question if there is an actual temporal coupling between them. If that's the case, a redesign of A and/or B seems to be a good idea as well.