Veni, vidi, vici, refactor

Love it? Share it!

Revealing Code Intent With Tests

Published last year in Best Practices - 0 Comments

A story

You join new software development team and are eager to learn about the project and make your first contribution. You spend most of your first day in meetings. After waaaay too much information about everything – domain, technologies and architecture, here you are (at last) – in front of your new computer, development environment set up and ready to write some code! Your task is simple – there are a few failing unit tests. Error logs show NullPointerException – a pretty common error, usually a quick fix. How hard could it be?

You take a look at the error and the code that fails. It is just as you have guessed. Fix is easy – just add a null check.It is tempting to do just that, but then you stop.

What is the intention of this code? What is it supposed to do? Maybe we do have a bug and the test is right to fail. You need to answer the question how should this code behave, not just verify what it does.

v1.0

You take a look at the code again. You pay some extra attention to the tests, searching for clues what is the code desired behavior. There are a few tests on that functionality, named something like “testRegistration” and “testWithInvalidEmail”. While the names and code give you some information, it is not enough and you need to look somewhere else.

In the end, you seek help from you colleagues. You feel bad for asking them for help so soon after you have started, but it could be important and you cannot proceed without knowing what’s going on. One of them points you to where the Test Cases documentation is. “Remember we talked about it on the meeting before lunch?” “Yeah, it was that meeting that was before the other meeting and after that other meeting, right?” Together you find out what the functionality should be. Now you know what the code intention is and you are good to go. You submit your fix with links to the Test Cases documents and a short explanation. After review you merge your change in source control on the next day. You are happy you managed to contribute.

v2.0

You take a look at code again. You pay some extra attention to the tests, searching for clues what is the code desired behavior. All the unit tests in the project follow a strange naming convention with rather long names, such as “regularRegistrationShouldDoThisWhenThat” and you even see some that are even using underscore_case, i.e “registration_with_facebook_should_do_that_other_thing_instead”. This is rather strange, as this is a Java project and this is not the official Java naming convention. It feels like reading another language to you. What those long names do yet they carry that domain information you were looking for. The underscore_case looks weird at first, but you realize it is much easier to read than the CamelCase. You understand not only what the code is doing, but also the problem solved. Maybe it is not a bad idea to write more tests like this in the future? You make a mental note to try it. Soon you submit your fix. Code review is quick and merge of your change is complete in a few hours. You are happy you managed to contribute.

Show the intent of the code

Good code will reveal intent.

It will show you what is the need behind the code.

When code reveals it’s intent, it is much more easier to work with it. Now you do not have to rely on other sources of information, your memory or just guess. You know why it was created in the first place.

If you want to write code that reveals intent, you first need to understand the purpose of the code that you are writing at the moment. And this is what it means to program deliberately.

Let’s get back to the above stories. You, the programmer, could have just added a null check. But this was not enough for you, you needed to know if this was the right thing to do.

You had to understand what is going on.

The only difference between the two versions is that first version code was not revealing intent. Yes, you had this information available somewhere else, but you needed time – both yours and your colleagues.

Save your future self time and reveal intent in your code.

Why in tests?

Tests are perfect for revealing intent:

  • They are executable;
  • They verify how your code is working;
  • They can serve as documentation.

When you write any code, test or production, you have the domain knowledge needed to understand what your code is supposed to be doing.

Document that domain knowledge in the tests and they become an executable documentation.

When you tests are executable documentation, they become your automated bug reports every time a test fails.

Whats with the naming convention?

I have given an example of a naming convention I use when I write tests in any language or framework. Take a look at this:

Registration should fail when mandatory field is not provided

Registration should succeed when all required fields are provided

Registration with Facebook should fail if user doesn’t have an Facebook account

What are these? These are your test cases. They follow the same pattern:

{ what we test } should { behavior } when { input }

{ what we test } could be a class in your code or not, depending on the type of the test you want to create.

{ behavior } is often a method in your code

{ input } is self-explanatory

Notice the word behavior. You document and execute behaviors, so this style is named Behavior Driven Development (BDD). I like it a lot, as I find it changes the way I think about my code and leads to more focused code that reveals intent.

Now what is more easy to read:

  1. What we test should behavior when input
  2. what_we_test_should_behavior_when_input
  3. WhatWeTestShouldBehaviorWhenInput

Issue with some languages and frameworks like Java’s JUnit is they do not give a good way to make the first one, so you need to use the_underscore or CamelCase. This is changing in JUnit 5, in case you are interested. Check out @DisplayName here.

Summary

  • Good code reveals intent;
  • You should document intent in your code;
  • Tests are perfect for documenting and verifying intent;
  • Treat tests as automated bug reports;
  • Naming convention for tests can help you reveal intent in an easy and consistent manner.

Subscribe to future posts