Welcome to Part 3 of this review of the Pluralsight course Introduction to Testing in Java by Richard Warburton.
Writing Good Tests
The purpose of this module is to explain what good tests look like and how we can achieve them.
Good Tests
Richard begins with the important point that tests are a trade-off. They have many important benefits, but there are also costs associated with writing them.
He explains Maintenance, Readability and Coupling are all costs that we need to be aware of. Tests are code.
Good Practices
Good tests:
– Are Well Named
– Test the Behavior not the implementation
– Don’t repeat themselves
– Output useful diagnostics
Richard explains why each of these points are important. A good name provides executable documentation, makes maintenance easier and improves readability.
We see some naming anti-patterns to avoid, and then see some examples of good naming.
There’s lots of advice available on the Internet for good naming. Some rules that Richard likes best are:
– Use domain terminology
– Natural language
– Be descriptive
Behavior Not Implementation
Richard says this is one of the hardest things to get right, and I agree. We want to test the behaviour of a test but avoid testing it’s implementation.
We should avoid exposing private state for testing purposes.
And we want to design our tests in a way that allows us to change the implementation details without breaking our existing tests.
Also covered here is the Don’t Repeat Yourself principle. We can eliminate this by encapsulating it into common methods or using Before and After which will be explained soon.
Finally, avoid magic numbers!
Diagnostics
Ideally we don’t want to run up the debugger every time a test fails. The output of the test should make the problem quite clear to us.
We should prefer assertEquals
over assertTrue
when we are comparing numeric values because this gives better diagnostic information.
Better still we can add messages to our output. Richard gives the following example:
List order = cafe.brewOrder(...);
assertEquals("Wrong quantity of coffee", 1, order.size();
If this fails it will output:
Wrong quantity of coffee
Expected :1
Actual :0
Live Coding
In this lesson we see the test canBrewLatte
failing with a unclear error message. Richard improves it so the output is better:
java.lang.AssertionError: Wrong coffee type
Expected :Latte
Actual :null
We see a similar improvement for the canBrewEspresso test.
Next, Richard highlights the duplication problem, and fixes it with an extract method refactor.
We also see how to turn the hardcoded number of beans, and other items, into constants.
Before & After
Code that commonly surrounds tests is for setting up and tearing down tests. To reduce or eliminate this duplication we can use Before and After annotations
@Before // Before each test method runs
@After // After each test method runs
@BeforeClass //Before all tests in the class
@AfterClass //After all tests in the class
Before & After (Live Coding)
Here we see how to use these annotations in practice. We start with simple methods that output messages to the console to describe themselves and see the order they run in is:
before class
before
after
before
after
before
after
before
after
before
after
after class
Then Richard shows us how we can use the before annotation in our CafeTest class.
Hamcrest
Hamcrest is a library for writing better tests by helping us avoid repetition and improve diagnostics.
It’s all based on matchers, which are blobs of logic used in assertions. If you’ve done unit testing in JavaScript before, you’ll probably have used matchers already.
In fact JUnit uses matchers as well.
Hamcrest (Live Coding)
We start by adding hamcrest-all as a project dependency. JUnit already ships with the Hamcrest core matchers, but to access all of the matchers we must add this.
With this added we can use assertThat. We can use powerful functions such as hasKey, hasItem and hasProperty as our arguments for this assertion.
Richard gives examples of using each of these.
Compositional matchers are matchers that take another matcher as a parameter.
assertThat(coffee, hasProperty("beans", equalTo(ESPRESSO_BEANS)));