How to write elegant unit tests in Spring Boot

Unit testing refers to checking and verifying the smallest testable unit in software. In Java, the smallest unit of unit testing is a class. Verify that the code under test conforms to expected results or behavior by writing small pieces of code against a class or method. Executing unit tests can help developers verify that the code correctly implements functional requirements and can adapt to changes in the application environment or requirements.

This article will introduce how to write elegant unit tests in Spring Boot, including how to add unit test dependencies, how to unit test components at different levels, and how to use Mock objects to simulate real object behavior. This article assumes that readers already have a certain understanding and foundation of Spring Boot and unit testing.

Table of contents

1. Unit test dependencies in Spring Boot

 2. Unit tests at different levels in Spring Boot

service layer

Controller layer

Repository layer

3. The use of Mock objects in Spring Boot

Summarize


1. Unit test dependencies in Spring Boot

In a Spring Boot project, to perform unit testing, you first need to add the corresponding dependencies. If you use Maven as a build tool, you can add the following dependencies to the pom.xml file:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

This dependency includes multiple libraries and functions, mainly the following:

  • JUnit: JUnit is the most popular and commonly used unit testing framework in Java, which provides a set of annotations and assertions to write and run unit tests. For example, the @Test annotation represents a test method, and the assertEquals assertion represents whether two values ​​are equal.
  • Spring Test: Spring Test is a Spring-based testing framework that provides a set of annotations and tools to configure and manage Spring contexts and beans. For example, the @SpringBootTest annotation represents an integration test class, and the @Autowired annotation represents automatic injection of a Bean.
  • Mockito: Mockito is one of the most popular and powerful Mock object libraries in Java, which can simulate complex real object behaviors, thus simplifying the testing process. For example, the @MockBean annotation means to create a Mock object, and the when method means to define the behavior of the Mock object.
  • Hamcrest: Hamcrest is a matcher library in Java that provides a set of semantically rich and readable matchers for result validation. For example, the assertThat assertion means to verify whether a value satisfies a matcher, and the is matcher means whether two values ​​are equal.
  • AssertJ: AssertJ is an assertion library in Java that provides a set of fluent and intuitive assertion syntax for result verification. For example, the assertThat assertion means verifying whether a value satisfies a condition, and the isEqualTo assertion means whether two values ​​are equal.

In addition to the above libraries, spring-boot-starter-test also includes other libraries and functions, such as JsonPath, JsonAssert, XmlUnit, etc. These libraries and functions can be selected and used according to different test scenarios.

 2. Unit tests at different levels in Spring Boot

service layer

The Service layer refers to the layer that encapsulates business logic and processes data, and it is usually identified using @Service or @Component annotations. In Spring Boot, to unit test the Service layer, you can use the @SpringBootTest annotation to load the complete Spring context, so that the Bean of the Service layer can be automatically injected. At the same time, you can use the @MockBean annotation to create and inject Mock objects at other levels, so as to avoid actually calling methods at other levels, but to simulate their behavior.

For example, suppose there is a UserService class that provides a method to query user information based on user ID:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public User getUserById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

To unit test this class, write the following test class:

@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserRepository userRepository;

    @Test
    public void testGetUserById() {
        // 创建一个User对象
        User user = new User();
        user.setId(1L);
        user.setName("Alice");
        user.setEmail("[email protected]");

        // 当调用userRepository.findById(1L)时,返回一个包含user对象的Optional对象
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));

        // 调用userService.getUserById方法,传入1L作为参数,得到一个User对象
        User result = userService.getUserById(1L);

        // 验证结果对象与user对象相等
        assertThat(result).isEqualTo(user);

        // 验证userRepository.findById(1L)方法被调用了一次
        verify(userRepository, times(1)).findById(1L);
    }
}

 

In this test class, the following key points and techniques are used:

  • Use the @SpringBootTest annotation to load the complete Spring context, and use the @Autowired annotation to inject the UserService object into the test class.
  • Using the @MockBean annotation means creating a Mock object of UserRepository and injecting it into the test class using the @Autowired annotation. This avoids actually calling UserRepository's methods, but simulates its behavior.
  • Use the when method to define the behavior of the Mock object, for example, when userRepository.findById(1L) is called, an Optional object containing the user object is returned.
  • Use the userService.getUserById method to call the method under test to get a User object.
  • Use AssertJ's assertion syntax to verify that the result object is equal to the user object. Various conditions and matchers can be used to validate the results.
  • Use the verify method to verify that the method of the Mock object has been called the specified number of times.

Controller layer

The Controller layer refers to the layer that handles user requests and responses, and it is usually identified using @RestController or @Controller annotations. In Spring Boot, to unit test the Controller layer, you can use the @WebMvcTest annotation to start a lightweight Spring MVC context and only load the components of the Controller layer. At the same time, you can use the @AutoConfigureMockMvc annotation to automatically configure a MockMvc object to simulate Http requests and verify Http responses.

For example, suppose there is a UserController class that provides an interface for querying user information based on user ID:

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        User user = userService.getUserById(id);
        if (user == null) {
            return ResponseEntity.notFound().build();
        } else {
            return ResponseEntity.ok(user);
        }
    }
}

To unit test this class, write the following test class:

@WebMvcTest(UserController.class)
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    public void testGetUserById() throws Exception {
        // 创建一个User对象
        User user = new User();
        user.setId(1L);
        user.setName("Alice");
        user.setEmail("[email protected]");

        // 当调用userService.getUserById(1L)时,返回user对象
        when(userService.getUserById(1L)).thenReturn(user);

        // 模拟发送GET请求到/users/1,并验证响应状态码为200,响应内容为JSON格式的user对象
        mockMvc.perform(get("/users/1"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id").value(1L))
                .andExpect(jsonPath("$.name").value("Alice"))
                .andExpect(jsonPath("$.email").value("[email protected]"));

        // 验证userService.getUserById(1L)方法被调用了一次
        verify(userService, times(1)).getUserById(1L);
    }
}

In this test class, the following key points and techniques are used:

  • Use the @WebMvcTest(UserController.class) annotation to indicate that only components of the UserController class are loaded, and components of other levels are not loaded.
  • Use the @AutoConfigureMockMvc annotation to automatically configure a MockMvc object, and use the @Autowired annotation to inject it into the test class.
  • Using the @MockBean annotation means creating a Mock object of UserService and injecting it into the test class using the @Autowired annotation. This avoids actually calling the UserService method, but simulates its behavior.
  • Use the when method to define the behavior of the Mock object, for example, when userService.getUserById(1L) is called, the user object is returned.
  • Use the mockMvc.perform method to simulate sending an Http request, and use the andExpect method to verify the Http response. Various matchers can be used to validate response status codes, content types, content values, etc.
  • Use the verify method to verify that the method of the Mock object has been called the specified number of times.

Repository layer

The Repository layer refers to the layer that encapsulates data access and persistence, and it is usually identified using @Repository or @JpaRepository annotations. In Spring Boot, to unit test the Repository layer, you can use the @DataJpaTest annotation to start an embedded database and automatically configure JPA-related components. At the same time, you can use the @TestEntityManager annotation to obtain a TestEntityManager object for manipulating and verifying database data.

For example, suppose there is a UserRepository interface, which inherits the JpaRepository interface, and provides a method to query the user list by user name:

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    List<User> findByName(String name);
}

To unit test this interface, you can write the following test class:

@DataJpaTest
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager testEntityManager;

    @Test
    public void testFindByName() {
        // 创建两个User对象,并使用testEntityManager.persist方法将其保存到数据库中
        User user1 = new User();
        user1.setName("Bob");
        user1.setEmail("[email protected]");
        testEntityManager.persist(user1);

        User user2 = new User();
        user2.setName("Bob");
        user2.setEmail("[email protected]");
        testEntityManager.persist(user2);

        // 调用userRepository.findByName方法,传入"Bob"作为参数,得到一个用户列表
        List<User> users = userRepository.findByName("Bob");

        // 验证用户列表的大小为2,且包含了user1和user2
        assertThat(users).hasSize(2);
        assertThat(users).contains(user1, user2);
    }
}

In this test class, the following key points and techniques are used:

  • Use the @DataJpaTest annotation to start an embedded database and automatically configure JPA-related components. This avoids relying on an external database and instead uses an in-memory database for testing.
  • Inject the UserRepository and TestEntityManager objects into the test class using the @Autowired annotation.
  • Use the testEntityManager.persist method to save the User object to the database. This prepares the test data without manually inserting the data.
  • Use the userRepository.findByName method to call the custom query method to get a list of users.
  • Use AssertJ's assertion syntax to verify the size and content of the users list. Various conditions and matchers can be used to validate the results.

3. The use of Mock objects in Spring Boot

In Spring Boot, in addition to using annotations such as @WebMvcTest and @DataJpaTest to load specific-level components, you can also use @SpringBootTest annotations to load a complete Spring context for more integrated testing. However, in this case, some problems may be encountered, such as:

        - The testing process needs to rely on external resources, such as databases, message queues, and Web services. These resources may be unstable or unavailable, causing tests to fail or time out.
        - The methods of other components or services need to be called during the test, but the implementation or behavior of these methods is uncertain or uncontrollable, resulting in unpredictable or inaccurate test results.
        - During the test, it is necessary to verify some results that are difficult to observe or measure, such as log output, exception throwing, private variable values, etc. These results may need to be obtained or verified using complex or intrusive means.

In order to solve these problems, Mock objects can be used to simulate the behavior of real objects. A mock object refers to a virtual object that replaces a real object during a test, and it can return a specific value or perform a specific operation according to preset rules. Using Mock objects has the following benefits:

        - Reduce test dependence: By using Mock objects to replace external resources or other components, the dependence on the real environment during the test process can be reduced, making the test more stable and reliable.
        - Improve test control: By using Mock objects to simulate specific behaviors or scenarios, you can improve the control over the behavior of real objects during the test process, making the test more flexible and accurate.
        - Simplified test verification: By using Mock objects to return specific results or trigger specific events, the verification of real object results or events during the test process can be simplified, making the test simpler and more intuitive.

In Spring Boot, to use a Mock object, you can use the @MockBean annotation to create and inject a Mock object. This annotation will automatically use the Mockito library to create a Mock object and add it to the Spring context. At the same time, you can use the when method to define the behavior of the Mock object, and the verify method to verify the method call of the Mock object.

For example, suppose there is an EmailService interface that provides a method for sending emails:

public interface EmailService {

    void sendEmail(String to, String subject, String content);
}

To unit test this interface, you can write the following test class:

@SpringBootTest
public class EmailServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private EmailService emailService;

    @Test
    public void testSendEmail() {
        // 创建一个User对象
        User user = new User();
        user.setId(1L);
        user.setName("Alice");
        user.setEmail("[email protected]");

        // 当调用emailService.sendEmail方法时,什么也不做
        doNothing().when(emailService).sendEmail(anyString(), anyString(), anyString());

        // 调用userService.sendWelcomeEmail方法,传入user对象作为参数
        userService.sendWelcomeEmail(user);

        // 验证emailService.sendEmail方法被调用了一次,并且参数分别为user.getEmail()、"Welcome"、"Hello, Alice"
        verify(emailService, times(1)).sendEmail(user.getEmail(), "Welcome", "Hello, Alice");
    }
}

In this test class, the following key points and techniques are used:

  • Use the @SpringBootTest annotation to load the complete Spring context, and use the @Autowired annotation to inject the UserService object into the test class.
  • Use the @MockBean annotation to create a Mock object of EmailService, and use the @Autowired annotation to inject it into the test class. This avoids actually calling the methods of EmailService, but simulates its behavior.
  • Use the doNothing method to define the behavior of the Mock object, for example, when the emailService.sendEmail method is called, do nothing. You can also use doReturn, doThrow, doAnswer, etc. methods to define other types of behavior.
  • Use the anyString method to represent an argument of any string type. You can also use anyInt, anyLong, anyObject and other methods to represent other types of parameters.
  • Use the userService.sendWelcomeEmail method to call the method under test, and pass in the user object as a parameter.
  • Use the verify method to verify whether the method of the Mock object has been called the specified number of times, and whether the parameters meet expectations. You can also use never, atLeast, atMost and other methods to indicate other times of verification.

In addition to using the @MockBean annotation to create and inject Mock objects, you can also use the @SpyBean annotation to create and inject Spy objects. A spy object refers to a virtual object that partially replaces a real object during a test. It can return a specific value or perform a specific operation according to preset rules while retaining other behaviors of the real object. Using the Spy object has the following benefits:

  • Preserve real behavior: By using Spy objects instead of real objects, other behaviors of real objects can be preserved, making the test closer to the real environment.
  • Modify some behaviors: By using Spy objects to simulate specific behaviors or scenarios, you can modify some behaviors of real objects, making the test more flexible and accurate.
  • Observing real results: By using Spy objects to return specific results or trigger specific events, you can observe the results or events of real objects, making the test more intuitive and credible.

In Spring Boot, to use a Spy object, you can use the @SpyBean annotation to create and inject a Spy object. This annotation will automatically use the Mockito library to create a Spy object and add it to the Spring context. At the same time, you can use the when method to define the behavior of the Spy object, and the verify method to verify the method calls of the Spy object.

For example, suppose there is a LogService interface that provides a logging method:

public interface LogService {

    void log(String message);
}

To unit test this interface, you can write the following test class:

@SpringBootTest
public class LogServiceTest {

    @Autowired
    private UserService userService;

    @SpyBean
    private LogService logService;

    @Test
    public void testLog() {
        // 创建一个User对象
        User user = new User();
        user.setId(1L);
        user.setName("Alice");
        user.setEmail("[email protected]");

        // 当调用logService.log方法时,调用真实的方法,并打印参数到控制台
        doAnswer(invocation -> {
            String message = invocation.getArgument(0);
            System.out.println(message);
            invocation.callRealMethod();
            return null;
        }).when(logService).log(anyString());

        // 调用userService.createUser方法,传入user对象作为参数
        userService.createUser(user);

        // 验证logService.log方法被调用了两次,并且参数分别为"Creating user: Alice"、"User created: Alice"
        verify(logService, times(2)).log(anyString());
        verify(logService, times(1)).log("Creating user: Alice");
        verify(logService, times(1)).log("User created: Alice");
    }
}

In this test class, the following key points and techniques are used:

  • Use the @SpringBootTest annotation to load the complete Spring context, and use the @Autowired annotation to inject the UserService object into the test class.
  • Using the @SpyBean annotation means creating a LogService Spy object and injecting it into the test class using the @Autowired annotation. In this way, the real behavior of LogService can be preserved while modifying some behaviors.
  • Use the doAnswer method to define the behavior of the Spy object, for example, when calling the logService.log method, call the real method and print the parameters to the console. Other types of behavior can also be defined using methods such as doReturn, doThrow, doNothing, etc.
  • Use the anyString method to represent an argument of any string type. You can also use anyInt, anyLong, anyObject and other methods to represent other types of parameters.
  • Use the userService.createUser method to call the method under test, and pass in the user object as a parameter.
  • Use the verify method to verify that the method of the Spy object has been called the specified number of times and that the parameters are as expected. You can also use never, atLeast, atMost and other methods to indicate other times of verification.

Summarize

This article describes how to write elegant unit tests in Spring Boot, including how to add unit test dependencies, how to unit test components at different levels, and how to use Mock objects and Spy objects to simulate real object behavior. This article also gives sample code for each type of unit test, and explains the key points and techniques.

By writing unit tests, the quality and stability of Spring Boot applications can be improved, and the programming level and confidence of developers can also be improved. I hope this article can help and inspire you, so that you can write elegant unit tests in Spring Boot.

Guess you like

Origin blog.csdn.net/TaloyerG/article/details/132487310