Press "Enter" to skip to content

Is my test easy to understand? – Testing #2

So you have tests, they are well named, well structured, with given, when, then blocks and all that beautiful things, they can even fit on a screen! But do you know how and what part of your code these tests test?


Part of a Testing series

  1. What makes a test good?
  2. Is my test easy to understand?

Input data

Majority of tests require some kind of input data to be provided since every algorithm basically transforms one form of data into another, even random generators need input data to generate something. Therefore to test such algorithms you have to provide this data. That’s an easy task, you create some object, new here, new there, set this, set that, construct few more objects and voila! You have input data prepared and you have even put it in the given block. Great! But there is a good chance, that you did end up with something like this:

As you see, this is a lot of code, and a lot of code requires a lot of time to analyze and understand it. You may understand it now, but in a month, you will probably completely forget about this test, and your fellow developers that will also stumble upon it will also be at loss.

This example is really simple, that’s true, there are only some constructors and setters. But object construction can be much more complicated.

Anyway, let’s try to simplify it a bit.

The first thing that you can do is to extract strings and other reusable parts. For example, dates, which take a lot of space and the precise date is not important for this test. Let’s assume, that for this test, we need a post with a comment, from which only IDs and dates are important for us.

It got a bit better. Now all input data is defined as final static fields in the Test class.

Next thing that you can do to simplify it more is to use some kind of builder for your objects. For example, using Lombok, it may look like this:

It’s getting better. But it’s still basically the same, it is much easier to read, but still, it is a big block of code. The biggest impact you will achieve when you will start creating your own language for tests. You can do this easily by creating methods with good names!

Take a look at this:

At a glance, you see exactly what is important. There is no noise. You can expand this in any form, with multiple variations that will suit your tests. For example, if you need multiple comments, you use it like this:

Both these methods are really simple, in essence, they are just wrappers for a code from the beginning:

Assertions

Tests are all about assertions, they are the most important part of the tests, so they should be easy to understand. Unfortunately, often they are not.

A popular bad habit that I have seen, is not using specialized assertion checking methods. For example, using boolean checks for all sort of things. Take a look at these few examples:

These are good, they do test. But the problem is, you have to carefully read them to understand what do they check and when will they fail the only message you will get is that on line XX you got false when you expected true. That’s not really helpful.

Now if you use dedicated assertions, you will get more information on what is wrong, like expected and actual values.

It gets even better if you use something like AssertJ, which has a really wide selection of different assertions that make things much, much easier to read.

Using assertions the correct way is one part of a solution. Often, when you make tests, you have to do many complex checks, like checking if various fields of object have correct value, collections have correct contents or even if various methods were called with specific arguments.

Doing all this often ends up as a big, unreadable block of assertions.

You can improve this by doing exactly the same as in the case of input data. Name the block by closing it in single or multiple methods. These methods can be named any way you want, they can take any parameters that you find useful for your case. For example:

Creating methods like this helps tremendously. All the implementation details are hidden,  so on the first glance, you can easily get the general idea of the assertions and the checks that they perform.

I think that important part here is naming. Keep your methods names concise and descriptive of what you expect. Do not use names like:

These names have too little information about what they check, what you expect, what should happen. The whole idea of using custom methods for assertions is for the sake of defining names for big blocks of code.

Mocks

The general idea with mocks is that the less the better. If you don’t have to mock, then don’t. If you can refactor your code so that classes will have less dependencies and as a results less things to mock then do it first.

Unfortunately, you often have to get along with mocking. Most of the time, mocking is not readable at all, even if it is only one thing to mock.

Above example is simple, but still it requires a considerable amount of effort to understand what it does. You can’t glance at it while skimming the failed test code and just know what it does.

As in previous cases, create a well named method for this mock.

The important thing here is how you name it, name it in such a way, that by reading it you will know what will happen and under what circumstances it will happen.

Using good names, also gives you an advantage of hidding multiple mock setups in single method, that will setup your whole system in single line. For example if you have multiple databases for different type of data and you need to have obtain object from them for you logic, you can make a single method like:

And inside you could then have:

As a side note, when you mock, try to be as precise as possible. By that I mean that you should use concrete values with eq(…) matchers, rather than using any() and anyString() etc. Doing this improves readability because by reading the mock definition you will see which parameter goes where, it will be easier for you to understand the flow of data. As a bonus, your test will be more sensitive to changes in the flow of data, it will detect more anomalies because your mock will return null when input value changes instead of your desired object as in normal situation.

Summary

If you use all the recommendations above to your tests, you will be able to shrink your tests considerably. You will be able to create something similar to the following test:

In the future I’ll write more in depth look into assertions and mocks, with examples of different use cases. But next, let’s talk about the test’s execution speed.