How to write good unit tests - Part III: The given part
© Image by Margarethe Pfründer-Sonn↗
In my former
post,
I’ve written about the Given - When - Then
pattern for unit tests.
In this post, we will have a closer look into the given
part:
The given
part’s job is making the test pass.
Sometimes there are no preconditions for the test, then the given
part
is empty.
This is a good thing!
Keep the given part of the test as small as possible.
business rules’ preconditions vs. preconditions caused by accidental technical complexity
Sometimes there are some technical preparations necessary for the test to pass. These preparations are no preconditions from a business rule’s point of view. They are just accidental complexity caused by the way we implemented things.
I usually put those preparations into a
setup
section or maybe extract them into a different method.
Why?
For a later reader it is relevant to understand:
Setup
: Do anything necessary to make the test execution reach
at least the first line of the then
part. You may change any
details as long as the test still passes.
Given
: This is part of the specification. When
changing things here, you essentially change the business rule.
So the pattern is now: Setup - Given - When - Then
.
Typical mistakes
mixing stubbing and verification
Most people use a mocking framework for faking dependencies, for example Mockito↗.
Sometimes I see people introducing implicit verifications by stubbing like this:
given(priceCalculator.vatProvider.getVat(NON_FOOD, GERMANY, date)).willReturn(0.19);
What if the implementation then calls?:
vatProvider.getVat(NON_FOOD, FRANCE, date)
You will get a NullPointerException. That is not what you want!
You want some error message like: “You should have sent GERMANY, but you have sent FRANCE.”
So for most cases: In the given
part, use the any()
matcher for stubbing; in the then
part, check for the expected values:
// given
given(priceCalculator.vatProvider.getVat(any(), any(), any())).willReturn(0.19);
// when
Money actual = priceCalculator.calcPrice(product);
// then
verify(priceCalculator.vatProvider).getVat(eq(NON_FOOD), eq(GERMANY), eq(date));
This gives you both:
- Better error messages, when the test fails.
- Explicit documentation of what is expected and what is provided.
btw: I prefer using BDDMockito, because it helps me not confuse the when
part vs. Mockito.when()
Too much usage of random values
I really love easy random↗ to create random values for my tests. That has some advantages:
- You can document that the value of a variable is not relevant.
- You can have some minimal checks that your code does not only work with specific values.
But sometimes I see things like:
// given
Product product = new EasyRandom().nextObject(Product.class);
// when
Money actual = priceCalculator.calcPrice(product);
// then
assert(actual).isNotNull();
It might be an extreme example, but sometimes reality is close to this. This test will even increase your line coverage metrics, but it does not really increase confidence in the correctness of the code.
Please: The first tests should use some example values that are a good representation of the real life values.
You can still add some randomization later.
Large setup/given
part.
Sometimes the setup
and given
parts grow and grow. Soon you have 30 lines of code for assembling sample data and doing all the stubbing.
You will find that the setup
and given
parts contain different levels of abstraction↗.
In the end you can’t see the forest for the trees.
To avoid large setup/given
parts you can:
- Extract the
setup
part into a different helper method. It is not necessary to understand the business rule. - In the
given
part: Use factory methods to assemble test data objects. Pass the relevant values as parameters. Example:
// given
Product product = createProduct(NON_FOOD, GERMANY, euros20);
Important business rules hidden in helper methods
The intention of the business rule should be readable without diving into helper methods.
So look at this test:
// given
Product product = createTestProduct();
// when
Money actual = priceCalculator.calcPrice(product);
// then
assert(actual.getPrice()).isEqualTo(Money.of(20, "EUR"));
So you ask yourself: Why 20? There must be some special values in the given product. So you need to dive into the factory method to see that the NET price was 16.81.
So better you write:
// given
Product product = createTestProduct(NON_FOOD, GERMANY, 16.81);
Then you can see all the relevant things necessary for understanding the test method.
My next post is about the then
part.
Any comments or suggestions? Leave an issue or a pull request!