Original address: https://reflectoring.io/unit-testing-spring-boot/
Well-written 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 provides you with a mechanism for writing a good unit test in a Spring Boot application, and in-depth technical details.
We will take you to learn how to create a Spring Bean instance in a testable way, and then discuss how to use Mockito
and AssertJ
, both of which are referenced by default in Spring Boot for testing.
This article only discusses unit testing. As for integration testing, testing the web layer and testing the persistence layer will be discussed in the next series of articles.
Code example
The code sample address attached to this article: spring-boot-testing
Using Spring Boot for testing series articles
This tutorial is a series:
- Use Spring Boot for unit testing (this article)
- Use Spring Boot and @WebMvcTest to test the SpringMVC controller layer
- Use Spring Boot and @DataJpaTest to test JPA persistence query
- Integration test through @SpringBootTest
If you like watching video tutorials, you can take a look at the Philip
course: Testing Spring Boot Application Course
Dependency
In this article, for unit testing, we will use JUnit Jupiter(Junit 5)
, Mockito
and AssertJ
. In addition, we will quote Lombok
to reduce some 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')
}
Mockito
and AssertJ
will be spring-boot-test
dependency, but we need to quote Lombok
.
Don't use Spring in unit tests
If you have used Spring
or Spring Boot
write unit tests before, you might say that we should not use Spring
when writing unit tests. But why?
Consider the following unit test class, which tests a single method of class RegisterUseCase
@ExtendWith(SpringExtension.class)
@SpringBootTest
class RegisterUseCaseTest {
@Autowired
private RegisterUseCase registerUseCase;
@Test
void savedUserHasRegistrationDate() {
User user = new User("zaphod", "zaphod@mail.com");
User savedUser = registerUseCase.registerUser(user);
assertThat(savedUser.getRegistrationDate()).isNotNull();
}
}
This test class takes about 4.5 seconds to execute an empty Spring project on my computer.
But a good unit test only takes a few milliseconds. Otherwise it will hinder the TDD (Test Driven Development) process, which advocates "test/development/test".
But even if we don't use TDD, waiting for a unit test for too long will spoil our attention.
It actually only takes a few milliseconds to execute the above-mentioned test method. The remaining 4.5 seconds is because @SpringBootTest
told Spring Boot
to start the entire Spring Boot application context.
So we start the entire application just because we want to RegisterUseCase
instance into our test class. It may take longer to start the entire application. Assuming that the application is larger and Spring
needs to load more instances into the application context.
So, this is why you should not use Spring
in your unit tests. Frankly speaking, most tutorials for writing unit tests do not use Spring Boot
.
Create a testable class instance
Then, in order to make the Spring
instance more testable, there are several things we can do.
Property injection is bad
Let's start with a counterexample. Consider the following classes:
@Service
public class RegisterUseCase {
@Autowired
private UserRepository userRepository;
public User registerUser(User user) {
return userRepository.save(user);
}
}
This class Spring
, because it does not provide a method to pass UserRepository
instances. Therefore, we can only use the method discussed earlier in the article-let Spring create UserRepository
instance of @Autowired
inject it into it through the 060c16b2c1d342 annotation.
The lesson here is: don't use attribute injection.
Provide a constructor
In fact, we don’t need to use 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) {
return userRepository.save(user);
}
}
This version allows constructor injection by providing a UserRepository
In this unit test, we can now create such an instance (or the Mock instance we will discuss later) and inject it through the constructor.
When creating the application context, Spring will automatically use this constructor to initialize the RegisterUseCase
object. Note that before Spring 5, we need to add the @Autowired
annotation to the constructor so that Spring can find the constructor.
Also note that the UserRepository
attribute is final
modified by 060c16b2c1d414. This is very important, because in this case, the content of this property will not change during the life of the application. In addition, it can also help us avoid becoming an error, because if we forget to initialize the property, the compiler will report an error.
Reduce template code
By using Lombok
of @RequiredArgsConstructor
notes, we can make the constructor automatically generated:
@Service
@RequiredArgsConstructor
public class RegisterUseCase {
private final UserRepository userRepository;
public User registerUser(User user) {
user.setRegistrationDate(LocalDateTime.now());
return userRepository.save(user);
}
}
Now, we have a very concise class, without boilerplate code, which can be easily instantiated in ordinary java test cases:
class RegisterUseCaseTest {
private UserRepository userRepository = ...;
private RegisterUseCase registerUseCase;
@BeforeEach
void initUseCase() {
registerUseCase = new RegisterUseCase(userRepository);
}
@Test
void savedUserHasRegistrationDate() {
User user = new User("zaphod", "zaphod@mail.com");
User savedUser = registerUseCase.registerUser(user);
assertThat(savedUser.getRegistrationDate()).isNotNull();
}
}
There is also part of it, how to simulate the UserReposity
instance that the test class depends on. We don't want to rely on the real class because this class requires a database connection.
Use Mockito to mock dependencies
The current de facto standard analog library is Mockito
. It provides at least two ways to create a simulated UserRepository
instance to fill the gaps in the aforementioned code.
Use ordinary Mockito
to simulate dependencies
The first way is to use Mockito programming:
private UserRepository userRepository = Mockito.mock(UserRepository.class);
This creates an object UserRepository
from the outside world. By default, the method will not do anything when it is called. If the method has a return value, it will return null
.
Because userRepository.save(user)
returns null, now our test code assertThat(savedUser.getRegistrationDate()).isNotNull()
will report a NullPointerException.
So we need to tell Mockito
to return something when userRepository.save(user)
We can use the static when
method to achieve:
@Test
void savedUserHasRegistrationDate() {
User user = new User("zaphod", "zaphod@mail.com");
when(userRepository.save(any(User.class))).then(returnsFirstArg());
User savedUser = registerUseCase.registerUser(user);
assertThat(savedUser.getRegistrationDate()).isNotNull();
}
This will make userRepository.save()
return the same object as the incoming object.
Mockito
provides many features for simulating objects, matching parameters, and verifying method calls. Want to see more, document
Annotate simulation objects with Mockito
of @Mock
Creating a mock object second way is to use Mockito
of @Mock
notes combined with JUnit Jupiter's MockitoExtension
used with:
@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {
@Mock
private UserRepository userRepository;
private RegisterUseCase registerUseCase;
@BeforeEach
void initUseCase() {
registerUseCase = new RegisterUseCase(userRepository);
}
@Test
void savedUserHasRegistrationDate() {
// ...
}
}
@Mock
annotation indicates which attributes require Mockito
injected into the simulation object. Since JUnit
will not be implemented automatically, MockitoExtension
tells Mockito
to evaluate these @Mock
annotations.
This result Mockito.mock()
method, you can choose according to personal taste. But please note that by using MockitoExtension
, our test case is bound to the test framework.
We can use the @InjectMocks
RegisterUseCase
attribute to inject the instance instead of manually constructing it through the constructor. Mockito
will use the specific algorithm to help us create the corresponding instance objects:
@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private RegisterUseCase registerUseCase;
@Test
void savedUserHasRegistrationDate() {
// ...
}
}
Use AssertJ to create readable assertions
Another library automatically attached to the Spring Boot
AssertJ
. We have used it to make assertions in the code above:
assertThat(savedUser.getRegistrationDate()).isNotNull();
However, is it possible to make the assertion more readable? Like this, example:
assertThat(savedUser).hasRegistrationDate();
There are many test cases, and only a small change like this can greatly improve the comprehensibility. So, let's create our 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() {
isNotNull();
if (actual.getRegistrationDate() == null) {
failWithMessage(
"Expected user to have a registration date, but it was null"
);
}
return this;
}
}
Now, if we assertThat
method from our custom assertion class UserAssert
AssertJ
library, we can use new and more readable assertions.
Creating such a custom assertion class may seem time-consuming, but it actually takes a few minutes. I believe that it is worthwhile to invest the time in creating readable test code, even if its readability only improves slightly afterwards. We write the test code only once, but after that, many other people (including me in the future) need to read, understand and manipulate these codes many times during the software life cycle.
If you still find it very troublesome, you can take a look at assertion generator
in conclusion
Although there are some reasons to start a Spring application during testing, it is not necessary for general unit testing. Sometimes it is even harmful because of the longer turnaround time. In other words, we should build Spring instances in a way that is easier to support writing ordinary unit tests.
Spring Boot Test Starter
comes with Mockito
and AssertJ
as test libraries. Let's use these test libraries to create expressive unit tests!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。