Adventures in Software Engineering

Finding Balance in Pursuit of the Perfectly Object-Oriented Unit Test

F

I know this may come as a shock to the more starry-eyed or idealistic amongst you, but in software engineering the pursuit of perfection is usually a bad thing. Reading the work of people who follow the extremes often leaves me with mixed feeling. Whilst I generally agree with their principles the outcome always seems distasteful. A recent example was an interesting post on Yegor Bugayenko’s blog where he argues that “a test method must contain nothing but a single assert”. That’s a fine principle, but not one I actually agree with and believe to be a classic example of rigidity over pragmatism. What really caught my eye though was how his trademark over-the-top drive for object-oriented purity saw him draw a conclusion that I feel actually defeated his own arguments.

Assert Once? Behave!

Right from the get-go let’s put this absolutist nonsense to bed. It is downright damaging to believe that one unit test should have only one assertion. To see this, consider an example where we need to ensure that a particular method is updating a collection parameter passed to it. We do this by exercising the code and checking for the existence of two expected values:

@Test
public void test() throws Exception {

    final List parameters = new ArrayList<>();
    new TypeUnderTest().update(parameters);

    assertTrue(parameters.contains("testParam1"));
    assertTrue(parameters.contains("testParam2"));
}

Does anybody seriously believe this requires two tests, one for each assertion? If the code is updated to add another three fields, should we have five unit tests? Of course not, in this simple scenario we’re testing the behaviour of a single method, and there is no conceptual benefit to additional code. This only brings duplication, maintenance overhead, increased runtime and log spam from a level of granularity that is far too fine. A common argument in favour of clear divisions is that the test will fail on the first assertion to fail, and so proceeding errors will not be revealed. So what? A unit test should be confined to a very small aspect of functionality, and any later failures not highlighted immediately by the build will be revealed to the poor fella lumbered with the bug. What matters is simply that this area is wrong. Where otherwise is a clear indication of larger problems such as the method handling too much, or merely delegating to disparate and perhaps unrelated methods. If that’s the case, then you have other things to be doing.

One Way Street Sign

The One, True Way

I generally find that strong proponents of doing things in certain ways can often become somewhat myopic, and Yegor’s example here is no different. Whilst the basic premise of his argument is clear and would be useful if not so literal, the desire to be yet more object-oriented does not support his conclusions. Let’s counter the three main arguments he makes in favour, in turn.

Reusability

This is absolutely premature optimisation. There would be merit to the idea were the code used by other test methods in the type, but it isn’t. So here the tight coupling of “algorithm” and “assertion” makes sense. The form of the data and the actions taken against it prior to the assertion are clear and restricted. As it stands there is no necessity for even a new method, but the rework to a static class is appalling. We now have an external dependency for this unit test that might be in future used by others, which of course entails potential refactorings that will require rework of existing tests and affect their execution and possibly their integrity. No. Just, no.

Brevity & Readability

This is not in and of itself a good thing. There’s no need to be long-winded, but the final examples Yegor provides actually raise the cognitive load. There used to be two variables prepared, and an assertion against them. Now I must now parse invocations of types within invocations, never having a clear understanding of what is happening unless I drill-down, and I must also filter out the wrapping assertion. You might like to think that code can be trusted as Yegor argues in the comments but this is abstract, puritanical thinking that just doesn’t work in the real-world. On the contrary, classes are like people. You should always afford the benefit of trust, but you should also be sure to count your change.

Concluding Thoughts

With even the most cursory of glances it is clear that this single assertion notion is not to be taken literally. Rather, the point is to limit tests to as small a conceptual area as makes sense. Not as small as possible, note — as small as makes sense. This implies a pragmatic approach to test creation, and taking the time to get it right not only helps to produce tests that provide a clear inside track on code’s behaviour, which is a large part of the reason they should exist, but also directly supports basic design principles in helping to ensure for example that methods and types are limited in their responsibilities.

The second a pursuit for a type of perfection interferes with the most basic of principles, such as producing code that is easy to read and maintain, you are out of the professional game and back into the amateur tinkering leagues. Just something to think about.

About the author

Johnathan Meehan
Johnathan Meehan

I’m a software engineer with more years and stomach under my belt than I would like. I have an odd sense of humour and a predilection for junk food, whiskey and beer. My first job was in 68K on God’s computer, the Commodore Amiga. Since then I’ve worked here, there and everywhere being paid to play with all kinds of fun things and once even nibbled around the edges of being an Apache committer. Most time now is spent with Java, and I put a heavy emphasis on quality. When I grow up, I want to be just like Oscar Mike.

Add comment

Adventures in Software Engineering

Johnathan Meehan

Johnathan Meehan

I’m a software engineer with more years and stomach under my belt than I would like. I have an odd sense of humour and a predilection for junk food, whiskey and beer. My first job was in 68K on God’s computer, the Commodore Amiga. Since then I’ve worked here, there and everywhere being paid to play with all kinds of fun things and once even nibbled around the edges of being an Apache committer. Most time now is spent with Java, and I put a heavy emphasis on quality. When I grow up, I want to be just like Oscar Mike.

Recommended Host