Code Quality in Test Code and Production Code
We all agree that we should not compromise on quality in test code. But code quality in test code has a different meaning in test code, than in production code.
Some things in production code are pretty fine, but we should not use it in test code. Some things in test code are pretty fine, but we should not use it in production code.
What’s the difference?
- Production code needs to be able to deal with user data that we have never expected.
- In test code, we are in control of all the data that goes through our code.
Loops (for each)
Loops are pretty fine in production code, but we should not use them in test code.
Why? Think of this test code:
actualList.forEach(it->
assertThat(it.getId()).isNotEmpty();
);
If the list is empty, then the test succeeds without verifying anything.
What’s better?
Let’s use AssertJ’s iterable assertions!
They have prepared methods (hasSize, contains, allMatch) that help us write expressive code.
Conditions (if/else)
Conditions are pretty fine in production code, but we should not use it in test code.
Let’s assume we use an if-else condition in production code. If we then use if-else in our test code, then our test code looks pretty much like the production code’s twin.
So we most likely make the same mistake in the production code AND in the test code.
What’s better?
Use parameterized tests!
- JUnit5 parameterized tests, Tutorials: https://www.petrikainulainen.net/programming/testing/junit-5-tutorial-writing-parameterized-tests/ and https://www.baeldung.com/parameterized-tests-junit-5
- TestNG Data Provider, Tutorial: https://www.toolsqa.com/testng/testng-dataproviders/
- Spock Data Tables https://spockframework.org/spock/docs/2.3/data_driven_testing.html#data-tables
instanceof and downcasts
Downcasting is considered as code smell.
Sometimes there’s a new implementation for an interface.
Then we need to adapt all occurrences of instanceof
and casts for the new implementation.
Sometimes we forget some occurrences.
Some people use the Visitor Pattern or Acyclic Visitor Pattern to avoid downcasts.
But IMHO, it is totally fine to downcast in the then
part of the test code:
There will never be a ClassCastException, because we have full control over all the types that the test code processes.
long method names
In production code, we avoid long method names. Some other code will call the method.
myvariable.anExtremelyLongMethodName();
Maybe the other code does this inside a block that adds more space in front. We do not want to exceed the monitor width.
Our test methods are only called by the framework. So we can use all the space:
@Test
void myImplementationShouldDoThisInCaseOfThosePreconditions() {
}
Please also consider to use @DisplayName and strings for documenting test methods.
Avoid beta versions?
The beta versions of libraries usually do not receive as much testing as released versions. So in production code, we avoid beta versions of libraries because we are afraid of hidden bugs.
That is no problem for test code. We are in full control of all the data that goes inside the code. So we can happily use beta version libraries in tests. If it works, it works.
My favourite resources on tests
- Gojko Adzic: “Specification by Example” ISBN 978-1617290084 https://gojko.net/books/specification-by-example/
- Jay Fields: Working Effectively with Unit Tests https://leanpub.com/wewut