Unit testing is probably one of the most discussed types of testing on the internet, with numerous blog posts, articles, and examples scattered around.
Unit testing is critical because, while it's relatively easy to understand and grasp, it offers a massive ROI in both simplicity and assurance that our code is working as intended.
This post will focus on presenting the JUnit framework used within the context of a typical Java project written using the Springboot framework. It will show how to set up test classes for services, the intended scope of unit tests, and how to perform TDD while doing local development, which is a natural consequence of having unit tests in place.
Let's dive in!
Table of contents
The Goal of Unit Tests
The main goal of unit tests, as the name states, is to test individual code units in an isolated fashion. What is a unit? As the name gives away, a unit is the smallest cohesive code unit that can be taken in isolation while being logically coherent and that is usually a method or a function.
In Java terminology, these would be methods of a specific class. The methods are what give the behavior to a class. In other words, the methods provide a class with the capability to do useful work by itself, as well as interact with other classes to produce any desired behavior.
As a logical consequence, the methods will provide the functionality when we look at a class. Still, these methods will have different levels of visibility and access with respect to other classes: there can be public, protected, and private methods.
It's usually stated the public methods will be what defines the public API of a given class, i.e., since only the public methods expose behavior to the outside world, we shall be only interested in writing unit tests for these specific methods.
Since the others are encapsulated within the class, the public methods will usually leverage them, so by testing the public-facing API of a given class, we will, by extension, test the entire functionality of the class.
Testing here means assessing that the behavior of each method does what is expected under all the possible scenarios it can realistically be called in production. Additionally, it also assesses that each method behaves as expected when things go wrong; unit tests must test both the happy, expected path, as well as unexpected or unhappy paths like wrong or missing inputs, error handling, etc.
Setting Up Our Small Data Model
Let’s now set up a JUnit test example scenario where we have a service for retrieving cats from a database. We will have some assumptions in place so that we can do the introduction to JUnit properly:
- The cat entity will have: a name, an age, and a color;
- We don’t interact directly with the database. Instead, we use the repository pattern for abstracting data access. This is a popular pattern to follow in a typical Springboot unit testing application. Essentially, a repository will be an interface where some automatically generated CRUD methods are available, and it’s very easy to work with:
@Repository
public interface CatRepository extends CrudRepository<Cat, Long> {
// Springboot will offer us, out of the box, implementations for CRUD methods to retrieve, update, save and delete Cat entities from our database
}
- The Springboot-managed service will have two dedicated public methods, for which we will want to write unit tests:
- one method will be called retrieveCats() and it’s a method that returns a list of Cat entities directly from the database;
- the other method will be called convertCatEntityToDTO(Cat cat), which receives an entity from the DB and essentially transforms it into a DTO object, suitable to be used by any client relying on any API endpoint that uses our service underneath. For our purposes, converting a Cat entity to a DTO essentially maps all the fields one-to-one of the entity to a DTO, except that instead of the age being a number, it will be transformed to a string like: “<X> year old cat”;
Now that we have introduced our small data model, we shall introduce JUnit, along with a small implementation of our service for example purposes.
Enter JUnit
Here is our service for which we will use JUnit testing Java with Springboot for:
@Service
public class CatService {
private CatRepository catRepository;
public CatService(CatRepository catRepository) {
this.catRepository = catRepository;
}
public List<Cat> retrieveCats() {
var catsFromDatabase = List.of(catRepository.findAll());
return catsFromDatabase;
}
public CatDTO convertCatEntityToDTO(Cat cat) {
return new CatDTO(cat.getName(), cat.getAge()+" year old cat", cat.getColor());
}
}
Now, by looking at these methods, we see that there is definitely some behavior we should write unit tests for:
- an empty database;
- a database with one single Cat;
- a database with more than one Cat;
- converting a “null” reference to a Cat must throw an Exception;
- converting a valid Cat entity reference must transform the age to the String in the expected format;
Before we do any of this, we need to configure JUnit as a dependency on our project, and typically, for Springboot projects, these will be available out-of-the-box, with dependencies like this one:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
These will be available in public indexes of such build system repositories like this one, for example.
Once this is in place, we can finally start writing our unit tests!
Writing Unit Tests
Usually, unit tests can be created right from your IDE by focusing on your production class. They can also be created via an IDE shortcut that will prompt you to name your test class, and, by default, it will be created in a package analog to the package of the production class, except that the top-level folder will usually be test.
JUnit in Springboot, in my professional experience so far, is always used together with the Mockito extension, which is an extension that allows you to programmatically configure mocks. Mocks are special classes that function as test harnesses for the real functionality we intend to test.
In other words, if we are only interested in testing the behavior of the retrieveCats() method, we don’t care exactly how the underlying repository class will function, because it serves only as a dependency for our method, and we can test it in isolation once we configure the repository to return a dedicated set of entities from the DB.
To set up our test class, this is how it could look:
@ExtendWith(MockitoExtension.class)
class CatServiceTest {
@Mock
private CatRepository catRepository;
private CatService catService; //service under test
@BeforeEach
public void setUp() {
this.catService = new CatService(catRepository);
}
@Test
public void when_no_cats_return_empty_list() {
Mockito.when(catRepository.findAll()).thenReturn(emptyList());
var catsFromDB = catService.retrieveCats();
assertTrue(catsFromDB.isEmpty());
}
@Test
public void when_one_cat_present_return_list_with_one_cat() {
Mockito.when(catRepository.findAll()).thenReturn(singletonList(new Cat("Bobi",3,"orange")));
var catsFromDB = catService.retrieveCats();
assertTrue(catsFromDB.size()==1);
assertEquals(catsFromDB.get(0).getName(), "Bobi");
}
The three most important principles to follow while writing unit tests with JUnit and Springboot are already illustrated in this short snippet: use Springboot annotations to hide a lot of bootstrapping complexity, use Mockito to stub any dependencies your services under test might require, and leverage JUnit annotations.
The annotation above lets the mocked dependency be initialized automatically by Mockito, which means that when we call the repository on our tests, we will not get a NullPointerException since the annotation will take care of initializing it for us.
Note how we pass it in the constructor of our service via a pattern called constructor injection. This means that once a dependency is passed in the constructor of our service, it will be initialized in the context of our service. When we run our test in our production code, the call to the repository method will be replaced by the value defined in the unit test with Mockito.
This powerful pattern allows us to write very expressive unit tests with little boilerplate code.
The other important pattern is the usage of the Mockito library itself to stub our dependencies. This is a very common and popular pattern. It allows us to stub our dependencies very easily in a way that combines very well with constructor injection to write expressive unit tests that read exactly like the production code which is a great benefit.
Last but not least, we are also using the JUnit special annotation @BeforeEach that needs to be placed under a special method called setUp(), which is a special JUnit configuration that does exactly what it promises. It will execute this setUp() method before running each test, enriching it with its context, which in our JUnit test case is the correct initialization of our service class.
There is also a static setup method, which means that it is a method that provides behavior at the class level, called @BeforeAll which will instead execute dedicated code before the tests run, only once. This is useful when we have, for example, some shared test data between all tests that can stay the same during the entire execution of all the tests. Then we can execute it only once and benefit from it during the tests execution.
This is how we can write JUnit tests in Springboot-powered Java applications.
Conclusion
We saw what unit tests are, their main goals, and how JUnit can be added to a Java project in this JUnit tutorial. Finally, we saw how Java developers can configure a test class for a Springboot project and write expressive unit tests that leverage the power of Springboot annotations with the expressiveness of JUnit.
If you want to read more java-related content, check out the following articles:
Java for Loops: Explained with Examples
JDK 17: The New Features in Java 17
Why Most Banking and Financial Institution Services Use Java