Thomas
Thomas

Reputation: 8978

What's a good way to unit test the main function that calls all the simple functions?

I have this code with lots of small functions f_small_1(), f_small_2(), ... f_small_10(). These are easy to unit test individually.

But in the real world, you often have a complex function f_put_things_together() that needs to call the smaller ones.

What is a good way to unit test f_put_things_together ?

func f_put_things_together() {
    a = f_small_1()
    if a {
        f_small_2()
    } else {
       f_small_3()
    }
    f_small_4()
    ...
    f_small_10()
}

I started to write tests but I have the impression that Im doing this twice as I have already tested the smaller functions.

I could have f_put_things_together take objects a1, a2, ..., a10 as arguments and call a1.f_small_1(), a2.f_small_2(), ... so that I can mock these objects individually but this doesn't feel right to me: if I didn't have to write unit tests, all these functions would logically belong to the same class, and I don't want to have unclear code for the sake of testing.

This is somehow language agnostic and somehow not, as languages like Python enable you to replace methods of an object. So if you have an answer that is language agnostic, that's best. Else Im currently using Go.

Upvotes: 1

Views: 678

Answers (1)

AggieEric
AggieEric

Reputation: 1209

The general case that you've shown in your example demonstrates the need to test both the simple functions and the aggregation of the results of those functions. When testing the aggregating function, you really want to fake the results of the smaller functions the aggregating function depends on. So, you're on the right track.

However, if you're having trouble writing unit tests for your code, then you're probably having one of these classes of problems:

  • You've somehow violated the SOLID principles (description here). In other words, something is deficient in the micro-architecture of your code.
  • You're trying to fake someone else's interface and you're having trouble matching the actual behavior of their implementation with your fake implementation. (This doesn't seem to be the case here).
  • The objects that you're testing with require a bunch of data setup that should be simplified, at least within the context of testing (also, doesn't appear to be the case).

If you're tests are painful to write, they're telling you something! With experience, you'll be able to quickly pick up on the pain point in your implementation that the tests are indicating.

Unfortunately, your example is a bit small and abstract. To be more precise, I don't know what f_small_1 ... f_small_10 do. So, with more details, I might make more precise recommendations for doing some small refactoring that could have a big payoff for your testing.

I can say, however, that it appears that f_put_things_together looks a bit big to me. This could be a violation of the Single Responsibility Principle (the 'S' in SOLID). I see 10 function calls at a minimum along with some branching logic.

You'll need to write a separate test for each branch path through your function. The less branching you have in a particular function, the less tests you'll need to write. For more information, take a look at Cyclomatic Complexity. In this case, it seems the method has a low CC, so this likely isn't the problem.

The ten calls to smaller functions do make me wonder a bit. It looks like for simplicity you've left out capturing the return value of these function calls and the logic for aggregating the results. In that case, yes you really do want to fake the results of the smaller functions then write a few tests to check the algorithm you're using to aggregate everything.

Or, perhaps the functions are all void and you need to verify that everything happened, and maybe that it happened in the right order. In that case, you're looking at writing more of an interaction-based test. You'll still want to put those smaller function calls behind an interface / class / object that you fake. In this case, the fake should capture the calls and the call order so that your test can make the assertions that are really important.

If some of the smaller functions are related to each other, it might make sense to group them together in a single method a separate class. Then, your test for f_put_things_together will have fewer dependencies that need to be faked. You will have a new class that also needs tested, but it's much easier to test two smaller methods than to test one large one that has too much responsibility.

Summary

This is actually a very good question with the exception of it being a bit vague. If you can provide a more detailed example, perhaps I could make more detailed recommendations. The bottom line is this: If your tests are difficult to write then either you need some help / coaching on writing tests or something about the design of your implementation is off and your tests are trying to tell you what it is.

Upvotes: 1

Related Questions