Imagine you have a group of friends coming over, and you decide to order pizza. Before you proceed with the order, you might ask yourself: How many people want a pizza? What type of pizza do they want? Does anyone have special requirements, such as vegan or no tomatoes? How do you know which pizzas to get? Keep in mind, it might take half an hour for your pizza to get there. You’re all starving, so you don’t want to have to send it back and wait even longer. What do you do?
You could play it safe and buy more than you think you need, but you’ll spend way more money, have to wait longer for the pizzas to be prepared, and risk your friends not liking what you ordered. The longer you wait to find out you’ve ordered the wrong pizzas, the hangrier everyone will get. 😩 If you ask your friends what they’d like, there might be some upfront squabbling, but at least you won’t have to send any pizzas back.
Software is the same. There’s nothing so soul destroying as investing days in writing software which ends up needing to be re-written because it didn’t do the right thing. In other words, ordering the wrong pizza. 🍕The users of your software also lose out as they have to wait for even longer for their feature. How do you avoid these issues?
Use TDD to avoid the costs of misunderstandings
Test-driven development (TDD) helps you avoid this. As you saw in the last chapter, with TDD, you create a new automated test before writing your code. To avoid confusion about what the software should be doing, it is important that your tests clearly explain what they are testing. This keeps your focus on solving one problem at a time and helps others on your team understand what are trying to achieve!
Why should I do it that way?
Lots of studies have shown that it is hundreds of times more expensive to fix a bug, the later it is found.
Using TDD to communicate what you’re testing, catches bugs while you are still coding. Sometimes before you even turn on your PC. TDD can save you from coding the wrong thing, or misunderstanding a test case.
This makes sense, right? If you find out that your code is wrong as you’re writing it, you can fix it immediately. TDD helps you to do this by making your test cases readable to others and starting you off with the confidence that your code does what it’s supposed to! If it takes a year to find out that your application has mistakes, you might have to go on a bug hunt through the whole codebase! This will take much longer!
How can I make my tests meaningful enough to help avoid mistakes? This is code, right?
Writing easy to read tests with Hamcrest
Did you notice how the test in the previous chapter used JUnit’s assertEquals
? Would you ever say "assert equals" in everyday life? Ever?
You: "Can I assertEquals that you're in good health?”
Your friend: ... 🤷🏻♀️
Would you assertEquals that the way we're talking is very robotic? Hey, people don’t talk like that! So why should your tests?
You want the tests to express what your software should be doing! Tests are tools for communicating with yourself in the future, as well as with others. If you understand how it should behave, then you know if it works!
You might be thinking that this is just code though. How are you supposed to get any clearer in an artificial language? There is a popular library called Hamcrest, which many Java projects use to make their test assertions a little more natural to read than JUnit’s assertions. It is designed so that your code flows naturally, and more closely resembles spoken language.
You can see the difference between Hamcrest and JUnit’s assertTrue
in this example. assertTrue
will only pass if the value you pass it is true. Let’s compare it with Hamcrest’s more expressive language:
Test case: A name is between 5 and 10 characters long. | ||
| JUnit Assertions | Hamcrest |
IN CODE |
|
|
READ ALOUD | “assertTrue name length greater than four and name length less than 11” | “assert that name length is greater than 4 and is less than 11” |
While JUnit assertions might look shorter, Hamcrest’s (aka matchers) read more naturally. You can describe simple and complex tests by putting them together in different orders, like the words you use every day. Having a rich vocabulary helps you express your ideas clearly. TDD is like thinking out loud when using matchers.
Let’s use Hamcrest to do some TDD!
Are you ready to have a go at using Hamcrest? Remember that test from the last chapter? Our calculator’s add method was tested with an assertEquals comparing two values. Let’s now add multiplication to the calculator:
Try reading this aloud. 🙄 Fortunately, Hamcrest has got our back here!
</p>
You’ll get Hamcrest’s most important matchers for free as they are included with JUnit 4. They just come together. If you use JUnit5, you’ll have to add a dependency for it as it’s no longer built into JUnit. Most projects still use JUnit 4 though.
Did you see the way we added more expressivity by combining the is
matcher with the equalTo
matcher? In the previous example, we also put an and
matcher with isGreaterThan
and isLessThan
matchers. Hamcrest helps you express what it is that you intend to test. This helps to keep you focused.
Let’s be honest. The language is still a little robotic, right? You can’t get away from this since Java is a programming language. The important thing is that you have a language which flows and can be used to describe your intent a little more naturally.
Let me give you some other examples. Imagine I had a set of animals and wanted to test that it contained cats and dogs.
I write: assertThat(animalSet, hasItems(“cat”, “dog”))
or assertThat(animalSet, contains(“cat”,
“dog”)
.
How natural it sounds is down to the matchers you use. These are just two of the many matchers provided by Hamcrest. They come in all shapes and sizes with lots of different vocabulary. Some of the more useful ones are:
Matcher | Example | What happens when your test fails? |
is() - Compares two values | assertThat(“a kitten”, is(“a kitten”) assertThat(“a kitten”, is( equalTo(“a kitten”)) | Expected: is "a dog" but: was "a kitten" |
equalTo() | assertThat(“a kitten”, equalTo(“a kitten”)) | Expected: "a dog" but: was "a kitten" |
not() | assertThat(“a kitten”, is( not( “a dog”) ) | Expected: is not "a kitten" but: was "a kitten" |
both(..).and(..) | assertThat(“a kitten”, both(is(“a kitten”).and(endsWith(“n”)) | Expected: (is "a kitten" and a string ending with "dog") but: a string ending with "dog" was "a kitten" |
Let's recap!
Test-driven development keeps your focus on solving one problem at a time.
By describing tests using clear language, you can catch any misunderstandings early.
The later you catch a defect or misunderstanding in your code, the more it will cost to correct.
Hamcrest matchers provides more readable assertions. You can put these together in different ways to clearly express what you are testing for.
Proper naming helps make our code easy to read and understand, but so do annotations! Intrigued? Find out more in the next chapter!