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:

  1. Unit testing with Spring Boot (this article)
  2. Test the SpringMVC Controller layer using Spring Boot and @webMvctest
  3. Test JPA persistence layer queries using Spring Boot and @datajPatest
  4. 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 normalMockitoTo 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

throughMockitothe@MockAnnotation 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!