0

In Python, consider a function like the following:

def main(*args):
    value1 = pure_function1(*args)
    if condition(value1):
        value = side_effect1(value1)
        if value:
            value2 = pure_function2(value)
            side_effect2(value2)
        else:
            value3 = pure_function3(value)
            value4 = side_effect3(value3)
            value5 = pure_function3(value4)
            side_effect4(value5)
    else:
        side_effect5()

Assuming that unit tests have been written for condition and all pure_functionX, which strategies are advisable to test the rest of the logic?

Writing unit tests with mocks seems cumbersome and hard to maintain because various mocks would be needed, and they have to be set up to cover all the possible branches (e.g. side_effect1 should be set to return both, truthy and falsy values). This can become even more complicated if more branches or deeper nested levels are involved.

On the other hand, testing all the paths via integration tests also carries their share of problems. Setting up the services/dependencies to cover all paths is often more complicated than doing it with mocks. Moreover, if the side effects are slow, the costs of running the tests could be prohibitive.

I was also exploring restructuring or creating an abstraction to deal with the issue, but I couldn't find a way to mitigate it meaningfully. Extracting a function/class that would cover the inner conditional would require passing several dependencies as parameters, and I don't see how it would simplify testing.

I also cannot find a way to separate the remaining decision logic from the side effects any further than it already is. I thought about converting the function to a generator containing only the branching and the pure functions, which yields some data object whenever a side effect is needed. The function executing the generator would then perform the side effect according to the data object, and pass the resulting value back to the generator. This approach involves introducing a function to run the generator, and I haven't seen it often used in Python, which makes me believe it can be found clunky and non-Pythonic.

Right now I am favoring a solution that mixes unit tests with mocks, and integration tests. Before going there, however, I would like to know if there is some obvious or common strategy to deal with this scenario that I am simply missing. I suspect this situation is found rather often.

Edit: to provide a bit better of context on what the side_effectX functions could be, they can be pictured as blocking network calls that perform an operation on a machine, e.g. open_valve(), start_engine(), increase_pressure(), etc.

1 Answer 1

3

Some quick things to mention:

  • The short answer here is to not write code with so many side effects. If you build a complex algorithm, it'll be more complex to manage. Don't build something you don't want to have to maintain.
  • Your code example is much too abstract and vague for us to derive any contextual information. This prevents us from being able to refactor your code as there is no meaning to any of it, so we cannot infer intended logical structure.
  • Since you're talking about mocks, I'm assuming that these side effects rely on external dependencies (i.e. the ones you'd be mocking). The answer is written under that assumption.

and they have to be set up to cover all the possible branches (e.g. side_effect1 should be set to return both, truthy and falsy values)

This is not correct. When mocking a dependency, you do not have to test for every possible return value. You only have to test for every possible behavior that your consumer can display based on the returned value.

The goal of testing a unit with a mocked dependency (with a focus on how it interacts with said dependency) is not to manually recreate all of the real dependency's behaviors. It is to test if the unit correctly uses its dependency. Therefore, you only need to test that which makes a difference to your unit. If your unit never behaves differently based on a returned value from the dependency, then there's nothing to gain from testing with several different mocked return values.

In your case, side_effect1 should indeed be mocked for both truthy and falsy, because it makes a difference in the if value behavior.
However, side effects 2, 3, 4 and 5 do not change the behavior of your unit, and therefore don't need to be tested with multiple values. The only meaningful test here is to confirm that the dependency is being called. What value it returns is irrelevant to the behavior of your unit.


Writing unit tests with mocks seems cumbersome and hard to maintain because various mocks would be needed,

On the other hand, testing all the paths via integration tests also carries their share of problems. Setting up the services/dependencies to cover all paths is often more complicated than doing it with mocks.

If you can't manage running the real code, and you can't manage unit testing and mocking the code; the logical conclusion here is that the code is not manageable. This suggests that your code is in dire need of a redesign, to untighten the coupling between all your pure functions and side effects.

Or, if you can prove that you do need to approach this exactly the way you have (e.g. if we're dealing with some very specific proprietary calculation, or if your employer simply refuses to spend time and effort on refactoring), then you're just going to have to suck it up and test it the way it is, even if writing all needed tests is cumbersome work.

2
  • I added edit my question to illustrate what kind of side effects I had in mind. Since the machine operation can be complicated, I think the complexity that the algorithm can exhibit is not necessarily artificial; it could be that the actual domain problem is complex.
    – pob
    Dec 9, 2021 at 10:08
  • @pob: A complex requirement does not mean your code must therefore be complex. It is the very nature of a developer to take a complex requirement and break it down into several smaller and more easily managed chunks. You've either not done this (sufficiently) for your code, or you have and therefore there is no further improvement to be made (and the testing complexity therefore is what it is).
    – Flater
    Dec 9, 2021 at 10:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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