Reflecmotor. IO/unity-testin…
Writing good unit tests can be seen as a difficult art to master. But the good news is that the mechanisms that support unit testing are easy to learn.
This article gives you a mechanism to write good unit tests in your Spring Boot application and delves into the technical details.
We’ll walk you through how to create Spring Bean instances in a testable way, then discuss how to use Mockito and AssertJ, both of which are referenced in Spring Boot for testing defaults.
This article deals only with unit tests. As for integration testing, testing the Web layer and testing the persistence layer will be discussed in the next series of articles.
Code sample
The code sample included with this article is at spring-boot-testing
Test the series with Spring Boot
This tutorial is a series of:
- Unit testing with Spring Boot (this article)
- Test the SpringMVC Controller layer using Spring Boot and @webMvctest
- Test JPA persistence layer queries using Spring Boot and @datajPatest
- Conduct integration tests via @Springboottest
If you’d like to watch video tutorials, check out Philip’s Course: Testing Spring Boot Applications
dependency
In this article, we will use JUnit Jupiter (JUnit 5), Mockito, and AssertJ for unit testing purposes. In addition, we’ll reference Lombok to reduce some of the template code:
dependencies{ compileOnly('org.projectlombok:lombok') testCompile('org.springframework.boot:spring-boot-starter-test') TestCompile 'org. Junit. Jupiter: junit - Jupiter - engine: 5.2.0' testCompile (' org. Mockito: mockito - junit - Jupiter: 2.23.0 ')}Copy the code
Mockito and AssertJ are referenced automatically in the spring-boot-test dependency, but we need to reference Lombok ourselves.
Do not use Spring in unit tests
If you’ve ever written unit tests using Spring or Spring Boot before, you might say let’s not use Spring when writing unit tests. But why?
Consider the following unit test class that tests a single method of the RegisterUseCase class:
@ExtendWith(SpringExtension.class)
@SpringBootTest
class RegisterUseCaseTest {
@Autowired
private RegisterUseCase registerUseCase;
@Test
void savedUserHasRegistrationDate(a) {
User user = new User("zaphod"."[email protected]"); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); }}Copy the code
The test class takes about 4.5 seconds to execute an empty Spring project on my computer.
But a good unit test takes just a few milliseconds. Otherwise, it will hinder the TDD (Test Driven Development) process, which advocates “test/develop/test”.
But even if we don’t use TDD, waiting too long for a unit test can ruin our concentration.
Executing the above test method actually takes only a few milliseconds. The remaining 4.5 seconds are because @SpringBoottest tells SpringBoot to launch the entire SpringBoot application context.
So we started the entire application simply to inject the RegisterUseCase instance into our test class. Starting the entire application may take longer, assuming the application is larger and Spring needs to load more instances into the application context.
So, that’s why you shouldn’t use Spring in unit tests. Frankly, most tutorials for writing unit tests don’t use Spring Boot.
Create a testable instance of the class
Then, there are a few things we can do to make Spring instances more testable.
Property injection is bad
Let’s start with a counter example. Consider the following categories:
@Service
public class RegisterUseCase {
@Autowired
private UserRepository userRepository;
public User registerUser(User user) {
returnuserRepository.save(user); }}Copy the code
This class cannot be unit tested without Spring because it does not provide methods to pass UserRepository instances. So we have to do what we discussed earlier in this article – have Spring create a UserRepository instance and inject it with the @Autowired annotation.
The lesson here is: Don’t use property injection.
Provide a constructor
In fact, we don’t need the @autowired annotation at all:
@Service
public class RegisterUseCase {
private final UserRepository userRepository;
public RegisterUseCase(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User registerUser(User user) {
returnuserRepository.save(user); }}Copy the code
This version allows constructor injection by providing a constructor that allows UserRepository instance parameters to be passed in. In this unit test, we can now create such an instance (or Mock instance, as we’ll discuss later) and inject it through the constructor.
Spring automatically uses this constructor to initialize the RegisterUseCase object when creating the build application context. Note that prior to Spring 5, we needed to add the @Autowired annotation to the constructor so that Spring could find the constructor.
Also note that the UserRepository property is now final. This is important because the content of this property does not change over the lifetime of the application. In addition, it helps us avoid becoming an error because the compiler will report an error if we forget to initialize the property.
Reduce template code
By using Lombok’s @requiredargsconstructor annotation, we can make the constructor automatically generate:
@Service
@RequiredArgsConstructor
public class RegisterUseCase {
private final UserRepository userRepository;
public User registerUser(User user) {
user.setRegistrationDate(LocalDateTime.now());
returnuserRepository.save(user); }}Copy the code
Now, we have a very compact class with no boilerplate code that can be easily instantiated in a normal Java test case:
class RegisterUseCaseTest {
privateUserRepository userRepository = ... ;private RegisterUseCase registerUseCase;
@BeforeEach
void initUseCase(a) {
registerUseCase = new RegisterUseCase(userRepository);
}
@Test
void savedUserHasRegistrationDate(a) {
User user = new User("zaphod"."[email protected]"); User savedUser = registerUseCase.registerUser(user); assertThat(savedUser.getRegistrationDate()).isNotNull(); }}Copy the code
Part of it is true, however, of how to simulate the UserReposity instance on which the test class depends. We don’t want to rely on the real class because it requires a database connection.
Use Mockito to simulate dependencies
The de facto standard simulation library is Mockito. It provides at least two ways to create a mock UserRepository instance to fill in the gaps described above.
Using normalMockito
To simulate dependencies
The first way is to program using Mockito:
private UserRepository userRepository = Mockito.mock(UserRepository.class);
Copy the code
This creates an object from the outside that looks like UserRepository. By default, methods are called without doing anything, and null is returned if the method has a return value.
Because userrepository.save (user) returns null, Now our test code assertThat (savedUser getRegistrationDate ()). IsNotNull () will be submitted to the null pointer exception (NullPointerException).
So we need to tell Mockito to return something when userRepository.save(user) is called. We can implement this using the static when method:
@Test
void savedUserHasRegistrationDate(a) {
User user = new User("zaphod"."[email protected]");
when(userRepository.save(any(User.class))).then(returnsFirstArg());
User savedUser = registerUseCase.registerUser(user);
assertThat(savedUser.getRegistrationDate()).isNotNull();
}
Copy the code
This causes userRepository.save() to return the same object as the one passed in.
Mockito provides many features to simulate objects, match parameters, and validate method calls. Want to see more documentation
throughMockito
the@Mock
Annotation mock object
The second way to create a Mock object is to use Mockito’s @Mock annotation in conjunction with JUnit Jupiter’s MockitoExtension:
@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {
@Mock
private UserRepository userRepository;
private RegisterUseCase registerUseCase;
@BeforeEach
void initUseCase(a) {
registerUseCase = new RegisterUseCase(userRepository);
}
@Test
void savedUserHasRegistrationDate(a) {
// ...}}Copy the code
The @mock annotation indicates those properties that Mockito needs to inject into Mock objects. Since JUnit is not implemented automatically, MockitoExtension tells Mockito to evaluate the @Mock annotations.
The result is the same as calling the mockito.mock () method, which is a matter of personal taste. Note, however, that by using MockitoExtension, our test cases are bound to the test framework.
We can use the @InjectMocks annotation on the RegisterUseCase property to inject instances instead of manually constructing them through constructors. Mockito uses specific algorithms to help us create instance objects:
@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private RegisterUseCase registerUseCase;
@Test
void savedUserHasRegistrationDate(a) {
// ...}}Copy the code
Create readable assertions using AssertJ
Another library that comes automatically with the Spring Boot test package is AssertJ. We already used it in the above code to assert:
assertThat(savedUser.getRegistrationDate()).isNotNull();
Copy the code
However, is it possible to make assertions more readable? Here’s an example:
assertThat(savedUser).hasRegistrationDate();
Copy the code
There are a lot of test cases out there where small changes like this can greatly improve understandability. So, let’s create our own custom assertion in test/ Sources:
class UserAssert extends AbstractAssert<UserAssert.User> {
UserAssert(User user) {
super(user, UserAssert.class);
}
static UserAssert assertThat(User actual) {
return new UserAssert(actual);
}
UserAssert hasRegistrationDate(a) {
isNotNull();
if (actual.getRegistrationDate() == null) {
failWithMessage(
"Expected user to have a registration date, but it was null"
);
}
return this; }}Copy the code
Now, if we import the assertThat method from our custom assertion class, UserAssert, instead of directly from the AssertJ library, we can use the new, more readable assertion.
Creating such a custom assertion class may seem time-consuming, but it can be done in minutes. I believe that the time invested in creating readable test code is well worth it, even if it only gets a little more readable later. We write test code once, but then many others (including my future self) need to read, understand, and manipulate that code many times over the course of the software lifecycle.
If you’re still having trouble, check out the assertion generator
conclusion
Although there are reasons to start a Spring application in testing, it is not necessary for general unit testing. Sometimes it’s even harmful because of the longer turnaround time. In other words, we should build Spring instances in a way that makes it easier to write common unit tests.
The Spring Boot Test Starter comes with Mockito and AssertJ as Test libraries. Let’s leverage these test libraries to create expressive unit tests!