I assume you already know technically how to write unit tests, and you are also applying the given-when-then pattern (a.k.a. arrange-act-assert) to make meaningful and understandable test cases.
As a professional developer, you will often extend existing behavior of the software and you copy and modify existing tests. But when you have to write a test for a new method from scratch, it can be hard to find the start and you finddle around ten minutes or more until you have the first running test – and in this case, you will very likely make mistakes that cost even more time to fix.
I want to present you three simple tricks to get you to a running test case in 30 seconds and then iterate on that, while you already see that you are going in the right direction.
Given – When – Then
Just a short reminder, so that we are all on the same page: We aim for tests that have three clearly separated parts: The “given” part contains all the setup of data and objects that we need for the test case. The “when” part is the call to the code under test, passing the prepared arguments and capturing the result of the call. The “then” part compares the observed behavior of the code under test to the expected behavior – this part should give us the feedback if the code works as expected or not.
Following the pattern helps you to focus on what the test is actually trying to proof; it prevents you from testing more then one thing at the same time, and also prevents you from writing meaningless tests (not testing anything at all).
Also, your fellow developers who are reading your tests later will have an easy time to understand your tests, when their eyes can locate the “when” part at first glance: This is the code under test, called under certain conditions and expected to show a certain output.
Now, to get to the topic, I prepared a small example for you: Let’s say we are working on a webshop software for computer equipment, and there are already database tables for product and order as well as a ProductRepository class with some methods and a ProductRepositoryTest class.
Our task is to add a new method findMostOrderedProducts and the respective test cases – we are going to work Test First, but that doesn’t really matter.
Here is the existing test class with the empty stub for our first new test:
Since there are already some test cases for simpler methods, we don’t have to worry about the test class setup – it’s already configured to run with Spring Boot, which creates a connection to the test databases, creates the Repository instance and injects it ready for us to use it.
But how do we start with our new test method now? The method that we are testing will be more complex than the existing examples – we will have to touch two database tables with a relation between them, so we need way more test data setup, right?
Tip no. 1: Start with the most simple case
The purpose of the method findMostOrderedProducts() is to look at the orders table, find the product ids that have the highest number of orders and then return the product data from the product table for these product ids. Also, the method takes in the number of products that we want it to return, so that we can look for the Top 5 or Top 10 or Top 100 products.
Since we have this idea of the method in mind, it’s intuitive to image the most common use case: There may be 20 orders for 5 different products, and when we ask for the Top 3 we should get those products that have the most orders.
But setting up this test case is a lot of work at the same time!
So what is the simplest test case? The case with the least amoint of setup required?
How about the case that there are no products and no orders in the database?
This does not sound like the most useful test case (and if we are in Test First mode, this case does not allow us to write a lot of production code), but it covers well-defined behavior of the method under test: If we call findMostOrderedProducts with the number 3, and there is nothing in the database, it should return the empty list.
That test case takes only seconds to write and run it. Then we can duplicate at modify it to cover the next simplest test case – how about the case that we have a product in the database, but no orders? And then, when we have more products in the database then the number that we are passing in? Should still return empty list.
By the way, we don’t have to keep the simplest case when we come up with the new one. If we think the second case makes the first obsolete, we can just enhance the first one instead of copying. That’s up to your judgement.
I decided to enhance the first test case twice and ended up with this:
Since I am working Test First, this test already allowed me to create a stub implementation for the method under test that is statically returning the empty list. If you work Test After and you have the implementation already, then the test demonstrates already that you didn’t forget the edge case of empty order list and your code will not throw exceptions.
Now it’s time for the “real” test: Does the method return the correct products if we have some orders? That will be a larger test, what is all the setup we need?
Tip no. 2: Start with the ‘when’
Let’s not worry about the test data setup for now. I know it’s intuitive to write the test from top to bottom line by line, but all we could do there is guessing – which means we would be probably wrong.
Instead, let’s write then “when” part first:
This is a no brainer – we know what we want to call and what parameters the method is going to take, so we write it down. Notice that we don’t yet specify the values of the parameters! The code doesn’t compile, because numberOfProducts is not yet defined. It looks unimportant here, but image a method with three different objects as parameters: By only making up variable names as parameters, without declaring and initializing them, I can write the “when” part in a second without thinking about the test data setup.
Next, I will write the “then”:
Also, this does not compile! I haven’t defined product1 and product2 yet. But I have completely expressed what this test should do. Now it’s pretty clear what has to go into the “given”, I don’t need to guess any more. First I need to make it compile:
Then I adapt the data to the scenario described in the test name:
As we expected, there is a lot going on in the “given” part. Anyway, after writing “when” and “then” it’s not hard to do.
See the test fail
In Test First mode, you will always execute the new test before writing the implementation. You want to see the test failing as expected; if the test surprisingly does not fail, you know there is something wrong with the test.
If you are working Test After, once you are done writing the full test it will pass (assuming your implementation is correct). The drawback is, you don’t have an indication if the test is correct – maybe you made a mistake writing the test, and it will always pass even if the implementation is wrong. To find that out, you should intentionally break the implementation for a moment (comment out a line or add a +1 somewhere), re-run the test and see it fail. Now you can be confident that you have written a meaningful test, and fix the implemeantation again.
Tip no. 3: Keep the tests short and self-contained
As we have seen, tests for complex logic can grow very long, because you need a lot of setup. (We haven’t yet included mocking for classes our method depends on…)
Since the instantiation of test data (products, orders…) will appear again and again in every test case, it’s tempting to move them out to a @Before or a field of the class / companion object. This will save you a lot of work in case you have to extend the data classes (Product, Order) with additional fields one day.
On the other hand, you want the test to be self-contained: Everything you need to understand the test should be right there, you shouldn’t be forced to scroll up or down and read the test data to understand why a specific string is passed to the method under test, or why a specific value is expected in the result; when those magic values depend on values you have set up in the test data on the other end of the source file, it’s hard to understand the tests.
Keep the balance between shorten the tests by separating stuff out, and leaving it self-contained by having every meaningful value right there in the test. A huge help for that can be private methods that you introduce to hide away the unimportant stuff and keep the important values right in the test:
In this case, what’s in the private helper funtion is completely unimportant to the reader of the test; but the fact that it’s called three times for product1 and so on, that is crucial to understand the test. Get creative and spot even more potential to extract out unimportant stuff there.