3

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?

1

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.

| improve this answer | |
0

assert that these methods were called in the required order

Tests are supposed to allow making changes easier, so that design mistakes can be corrected. They are not supposed to set design mistakes in stone.

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 only reason to offer these separate methods from A and B is if they can be called in different orders. If they can, a test shouldn't be dictating an order. If they can't, then A and B should stop inviting trouble. And some descent names would give us some idea what they're supposed to do.

class ArgumentDispatcher(object, A, B):
...
def dispatchArguments(self, args):
    ...  # Code which transforms args into arg1, arg2, ..., argN
    self.A.dispatchArguments(arg1, arg2, arg3)
    self.B.dispatchArguments(arg1, arg2, arg5, arg6)
    ...  # Code which either does something useful with arg7 ... argN 
    ...  # or rejects them somehow rather than silently ignore them.

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.

If all these methods do is dump the args to IO then there isn't enough behavior here to test. If they so much as add them together though get a test on that.

| improve this answer | |
0

my_method first does some straightforward work to transform the values it is given. I am not concerned with how to test this aspect.

But this is the important part of my_method.

calling a bunch of methods in the right order is almost too simple to fail and testing that will lead to stupid repetition of the production code in the test. Also this order is not likely to change for random reasons. If the order of calling the methods is important you could documents that in the names of the methods.

But calculations based on the inputs are much more fragile. These should really be secured by UnitTests.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.