Architecture,  Unit test

HOW DO I WRITE A UNIT TEST THAT IS READABLE? DESIGN PATTERNS IN TESTS.

Writing unit-tests is not only about having a safety harness that catches the bugs that we create. It is just as much about making it easy for us to find the bugs. We want to write the tests in a way that they point us to the bug with as little effort from our side as possible. We want the tests to do as much debugging for us as it can. And of course, there are design patterns for this too.

Code is never finished. We always add more features, change things, or just refactor. It’s part of our normal workday. But changing tested and delivered code is risky. Code is complex and it is easy to break things, even with the smallest changes. We all know that.

We used to say: “Don’t fix it if it isn’t broken.” The risk of adding problems was often too great compared to the gain we got from say, refactoring the code. And that eventually leads to stale code that nobody wants to touch even when the changes are necessary?

The full sentence really is: “Don’t fix it if it isn’t broken, because if you do, you will have to spend time testing and debugging.”

That is no longer a problem.

We have started to make the computers do the job for us. They are faster, they are more accurate, and they never get tired of doing it, never gets bored and careless.

Yes, we have figured out ways to get around this problem. And I submit to you that unit tests are a big part of this. We’ve found a way to not have to spend time testing and even reduce the time we spend on debugging.

Now, it doesn’t happen by itself. Not yet at least. Artificial Intelligence is around the corner, it will eventually relive us from this job too, but not yet. It is still up to us developers to write the unit-tests, to create that code that finds the problems for us.

Make! It! Readable!

A unit test has two sides. One for the computer that executes it and creates reports. The other side is aimed at us humans. As soon as the computer has done its part and found a problem, we take over to fix it.

But if we have to struggle to understand what the computer is telling us, we don’t gain as much from the help as we could.

The one thing that reduces the struggle most, is to make the tests readable. And I mean readable from the perspective of debugging, for someone that is trying to understand what the code does so, we can find where it no longer does that.

It all comes down to design patterns

A design pattern is something that emerges when we have done the same thing over and over, and we start to realise that we do things more or less in the same way every time.

Some patterns are demanded by the compiler. For instance, in C# we have to write a class in a certain way. We have to use gul wings, you know, “{  }” everywhere, and so on.

But other patterns are how we decide to organize the code, like dependency injection or CQS. Or just that we always use the same name on the variable that we return from a function, like “result”.

Patterns make us see things without having to think about it, so we can think about the problem instead. They help us recognize things. Just as our favourite supermarket chain that uses the same layout in every shop. We know where to go to find what we are looking for.

They act as filters; the brain is good at pattern matching and it lets us filter out what is irrelevant unconsciously, so we can spend the limited bandwidth that the active thinking has, on the relevant parts of the problem.

To succeed in writing readable tests, we have to use these patterns.

Only test one thing at a time

One of the most obvious things to reduce complexity is to only test one thing in each test. Yes, this will result in more code and that is the trade of for this pattern. A trade of I gladly make.

For instance, when the computer tells us that the string returned is empty when it shouldn’t, we know exactly where to look. But if we on the other hand tested everything in one single function, we would have to start with analysing and debugging the test to find where the problem actually is, before we know where to look for the problem. In this case, the computer only tells us that there is a problem. Not what it is. The vital information is missing.

Testing one thing per test makes the test point to the problem and many times we only need to read the name of the test function to know what went wrong and where to debug the code.

It can’t get more specific than that.

Create a test function name

This requires us to name the test function in a way that it clearly tells us what we are testing. If we don’t, we’re back to decoding the test to understand what it does. A good name is like a headline for an article. It tells you what the content is.

A proper test function name lets us zoom in on the problem. For instance, we could see that Add() the function didn’t return the correct sum.

A good test function name has a few attributes. It describes the functionality that is tested. We’re not as interested in the code that we test, as much as what the result the function delivers. In the end, that is what counts. If we are testing a calculator, we want to know that the addition works, not that the code uses a plus sign to do the addition. So, this should be reflected in the naming of the test functions too.

Also, this decouples the code from the name. If we say in the test name that a plus sign is used, and we the change the code to use bit shifting to do the addition, we will have to remember to change the name of the function too. And take it from me, we will forget. Plus, we really don’t want the extra work it gives. That just costs time and money, and is just boring work, when we have more important and creative things to do.

There are a few patterns on how to format a test function name, that are common in the developer community. Use whatever you and your team likes the best. There aren’t really one that is better than the other. It is totally subjective.

  • Write a sentence that describes the functionality and its outcome, using CamelCase:
    AddingTwoPositiveValuesReturnsTheSum()
  • Write a sentence that describes the functionality and its outcome, separating the words with underscore:
    Dividing_With_Zero_Throws_An_Exception()
  • Divide the function name into three sections, first the name of the function that is tested, second the functionality that is tested, and third, the expected outcome.
    Save_GivenAValidUser_TheDataStorageIsUpdated()

Examples explained

As you see in the examples above, none of them talk about how to do things, they only talk about what is expected to happen. Adding two positive values can actually be done in a few different ways. Using the + sign, a long row of ifs that match two values and return the sum, or bit shifting. We just don’t know how the function is realized, and we don’t want to know.

The second example ends with …Throws_an_Exception. That sounds like a technical detail. And sure, in a sense you’re right. But this is one of the ways that the language can return a value, we have to deal with it. However, notice that I didn’t say which exception.

This could be up for debate. The type of exception could be a part of the requirements. Then you should mention it. But if it is the developer who chose it, which it probably is in most cases, then I would leave the type out of the name. I’d say that then it is a technical detail, and it could change at any time.

The third has the name of the function called in the name. That is definitely a technical detail. And I think this is mostly a remain from yesterday when the tests systems weren’t so good at allocating and showing the failing tests. I mean, if you get a list on a console with test names, the only way you could know which function failed was by reading it in the function name. Now, you just double click on the test and it takes you directly to the code.

Me? I mix and match. Some projects I feel like one pattern, others another. But mostly, I have to adapt to the project that I am working on, because I come in midways and the pattern was chosen already. And it is not uncommon for larger companies to have all of them I their code, because nobody decided when they began, and every developer just used the one that is their preference.

Break it up arrange, act…

Let’s look at how we write the actual test. When we look at tests in general, we will see a pattern emerge. We will see that there most often are three different types of code.

First, we set up the test. We create the class we’re going to test. Then we create the test data and the data we’re expecting to find.

Second, we execute the test and collect the result of the test. Yes, I mean the return value. Or exception (which basically is a return value).

Third, we analyse what happened, and verify that it did what was expected.

So, let’s break the function up in these sections formally. The simplest way to do this is to add three comments to every test function:

// Arrange
// Act
// Assert

What this does for us is to create the filter I was talking about previously. The computer used this function to detect an error, so we read the function title. It says that adding two numbers should result in the sum. Now, this obviously didn’t happen. So, what is the problem then?

Now, where should we look to find more information? I would be pretty interested in what the addition actually returned. I would want to look at the analysis because that will tell me what the computer found and why it thinks it is wrong.

Do we find that in the code that sets up the test? No. What about the code that calls the addition function? Nope. Not that either.

Because we have the comments, we know not to look at the Arrange and Act sections. We can automatically skip that code and move directly to the Assert section to find what we’re looking for.

Actually, the brain will simply skip the Arrange and Act for us. We will probably not even see it.

This is the power of a pattern. Now, I would recommend that you always have these three comments in your tests to enforce the pattern and never have two patterns competing with each other.

Well, maybe when a test really only is a one liner, then it’s probably OK to break the pattern.

Regions

I have actually started using #region instead of comments. This is probably quite controversial. Regions have gotten a bad name because people use them to break up code within a function that should have been put in separate functions, and instead end up with huge functions that has way more than one responsibility and is really hard to test. Just bad practice. And because this is so common, people generally associate regions with bad architecture.

Now, as with everything, you can use it or abuse it. And in tests I use it to enhance the filters. The great thing with regions is that they can be collapsed. And many times, the setup part of the test can become quite huge. For instance, initializing large and complex objects takes up a lot of space. Collapsing this code and hiding it makes the test a lot more focused on the part you really want to look at. The only code that you will see is the test name and the assert part, which you obviously expanded. Simply less clutter.

Naming stuff

A test uses a lot of variables, and they are used for different things. If we analyse them, we’ll find a pattern here too. If we name the variables in a way that we can group them together mentally, we can let the brain find the patterns we’re looking for, again without us actively thinking about it.

Here is an example of a short test.

[Test]
public void AddingTwoValuesReturnsTheSum()
{
    // Arrange
    var requestedTerm1 = 1;
    var requestedTerm2 = 2;
    var expectedResult = 3;

    var sut = new TheMathClass();

    // Act
    var result = sut.Add(requestedTerm1, requestedTerm2); 

    // Assert
    Assert.That(result, Is.EqualTo(expectedResult));
}

The SUT

SUT stands for System Under Test. This is the instance of the class that contains the function that we are testing. Always name it sut and your brain will always be able to differentiate it from the rest. It is always there, so make it look the same way all the time.

Result

The function often return something. Always call this variable result. Where is the data we’re testing? We know this instinctively.

Dummy

When we set up a test, we many times have to provide data that the function needs to execute, but that really doesn’t affect the test at all. If we could, we would have left it out. But that’s not possible. The second best thing is to mark it as not part of the test. We do that by naming these variables starting with the word dummy.

Whenever we read a test and see a dummy variable, we immediately know that it’s not part of the test and we don’t have to spend any time on it. We simply filter it out.

Requested and expected

There are two kinds of test variables. The ones that we send in and the ones we use to compare with what we got out of the function, and I like to make this apparent. I use this schema where I name every variable that is sent into the function as requestedXxx, and the ones that verifies the result of the function, I name expectedXxx.

True, expectedXxx is only used once in this example, but I still think it’s a good idea to put it in a variable, for consistency throughout all your tests. But if you’d rather just put 3 in the assert, not a problem. But you give up some pattern matching to do so. But if you use expected all the time, you collect all test data in the arrange section, which in itself is part of the arrange pattern. Do what fit you and your team best. Your choice.

Self documenting code

Using this naming scheme makes the code tell us, without a doubt, what it is meant for. This part is for verification. This is for setting up the call with the correct start values. And this is just dead weight. It makes the code self-documenting. And that is something to strive for. That is a really good design pattern, not only in tests.

Use the patterns that work for you

This is some of the more common patterns. There are of course of many others. But I think this is a good start, and I think that you will find others if you just start looking.

Now, don’t think that this is written in stone. If you find that this works for you, use it. If not, don’t. Or better yet, create your own patterns. Do what work best for you. But remember, if you wander too far away from standards, you will be quite lonely. Others may have a hard time accepting your ways of doing things. Yes, I’ve tried that too.

— Cheers!

Like it? Share it!

Leave a Reply

Your email address will not be published. Required fields are marked *