头图

[Note] This article is translated from: Unit Testing with Spring Boot-Reflectoring

Well-written unit tests can be considered a difficult art to master. But the good news is that the mechanisms that support it are easy to learn.
This tutorial provides you with these mechanisms and details the technical details necessary to write good unit tests, with a focus on Spring Boot applications.
We will look at how to create Spring beans in a testable way, and then discuss the usage of Mockito and AssertJ, which are included by default in Spring Boot for testing.
Please note that this article only discusses the unit test . Integration testing, Web layer testing, and persistence layer testing will be discussed in subsequent articles in this series.

 Code example

This article attached on GitHub working code examples.

Dependency

For the unit tests in this tutorial, we will use JUnit Jupiter (JUnit 5), Mockito, and AssertJ. We will also include Lombok to reduce some boilerplate code:

dependencies {
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
    testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}

Mockito and AssertJ are spring-boot-starter-test dependency, but we must include Lombok ourselves.

Don't use Spring in unit tests

If you have used Spring or Spring Boot to write tests before, you might say that we don't need Spring to write unit tests . why is that?
Consider the following "unit" test that 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 takes about 4.5 seconds to run on an empty Spring project on my computer.
But a good unit test only takes a few milliseconds . Otherwise it will hinder the "test/code/test" process driven by test-driven development (TDD) ideas. But even if we do not adopt TDD, waiting too long for the test will destroy our attention.
It actually only takes a few milliseconds to execute the above test method. The remaining 4.5 seconds is due to @SpringBootRun telling Spring Boot to set up the entire Spring Boot application context.
So we launched the entire application just to RegisterUseCase instance into our test . Once the application becomes larger and Spring has to load more and more beans into the application context, it will take longer.
So, why shouldn't we use Spring Boot in unit tests? To be honest, most of this tutorial is about writing unit tests without Spring Boot.

Create a testable Spring Bean

However, we can do something to improve the testability of Spring beans.

Field injection is not advisable

Let's start with a bad example. Consider the following classes:

@Service
public class RegisterUseCase {

    @Autowired
    private UserRepository userRepository;

    public User registerUser(User user) {
        return userRepository.save(user);
    }

}

This class cannot be unit tested without Spring because it does not provide a way to pass instances of UserRepository Then, we need to write the test in the way discussed in the previous section, let Spring create an UserRepository and inject it into the field annotated @Autowired

The lesson here is not to use field injection .

Provide constructor

In fact, we should not 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 constructor that allows the UserRepository instance to be passed in. In the unit test, we can now create such an instance (probably a mock instance that we will discuss later) and pass it to the constructor.
When creating a production application context, Spring will automatically use this constructor to instantiate 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 field is now final . This makes sense, because the field content will never change during the life of the application. It also helps to avoid programming errors, because if we forget to initialize the field, the compiler will report an error.

Reduce boilerplate code

Using Lombok's @RequiredArgsConstructor annotations, 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 with no 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();
    }

}

However, one thing is still missing, that is how to simulate the UserRepository instance that our tested class depends on, because we don't want to rely on the real thing, it may need to connect to the database.

Use Mockito to simulate dependencies

The current de facto standard mock library is Mockito . It provides at least two ways to create a simulated UserRepository to fill in the gaps in the previous code example.

Use normal Mockito to mock dependencies

The first method is to use Mockito programmatically:

private UserRepository userRepository = Mockito.mock(UserRepository.class);

This will create an object UserRepository By default, when a method is called, it does nothing. If the method has a return value, it returns null .
Our test will now fail with NullPointerException assertThat(savedUser.getRegistrationDate()).isNotNull() , because userRepository.save(user) now returns null .
So, we must tell userRepository.save() to return something when calling 0617f907c94d10. We use the static when method to do this:

    @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 user object passed to the method.
Mockito has more functions to simulate, match parameters and verify method calls. For more information, please see the reference document .

Use Mockito's @Mock annotation to simulate dependencies

Another way to create mock objects is to combine Mockito's @ Mock annotation with JUnit Jupiter's MockitoExtension :

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

    @Mock
    private UserRepository userRepository;

    private RegisterUseCase registerUseCase;

    @BeforeEach
    void initUseCase() {
        registerUseCase = new RegisterUseCase(userRepository);
    }

    @Test
    void savedUserHasRegistrationDate() {
        // ...
    }

}

@Mock annotation specifies the fields that Mockito should inject into the mock object. @MockitoExtension tells Mockito to evaluate those @Mock annotations, because JUnit does not automatically perform this operation.
The result is the Mockito.mock() . Which method to use is a matter of taste. But please note that we bind our test to the test framework MockitoExtension
Please note that we can also use the @InjectMocks registerUseCase field instead of manually constructing the RegisterUseCase object. Then Mockito will create an instance for us according to the specified algorithm

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private RegisterUseCase registerUseCase;

    @Test
    void savedUserHasRegistrationDate() {
        // ...
    }

}

Use AssertJ to create readable assertions

Another library that Spring Boot test support automatically comes with is AssertJ . We have used it above to implement our assertion:

assertThat(savedUser.getRegistrationDate()).isNotNull();

However, wouldn't it be better to make the assertion more readable? E.g:

assertThat(savedUser).hasRegistrationDate();

In many cases, a small change like this will make the test easier to understand. So let us test source folder create our own custom assertion :

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 UserAssert class instead of the AssertJ library, we can use the new, easier-to-read assertions.
Creating a custom assertion like this may seem like a lot of work, but it actually only takes a few minutes to complete. I firmly believe that it is worth investing the time to create readable test code, even if it is only slightly more readable afterwards. After all, we only write the test code once, and other people (including the "future me") must read, understand and manipulate the code many times during the software life cycle.
If you still feel that the workload is too large, please check AssertJ's assertion generator .

in conclusion

There is a reason to start a Spring application in a test, but it is not necessary for a normal unit test. It is even harmful due to the longer turnaround time. Instead, we should build our Spring beans in a way that is easy to support writing simple unit tests for them.
Spring Boot Test Starter comes with Mockito and AssertJ as test libraries.
Let's use these test libraries to create expressive unit tests!
Sample code may be in final form on GitHub found.


信码由缰
65 声望8 粉丝

“码”界老兵,分享程序人生。