preface

This article is based on JUnit5. I hope you can learn how to test your own Spring Boot projects and how to pay attention to the testability of your code.

Spring Boot UnitTest

Spring Boot provides many useful tools and annotations to help us complete unit testing, including two modules: Spring-boot-test and Spring-boot-test-autoconfigure. We can introduce these two modules by relying on spring-boot-starter-test, which includes JUnit Jupiter, AssertJ, Hamcrest,Mockito, and other useful unit testing tools.

A simple example

@SpringBootTest
class UserServiceTest {

    @Autowired
    UserService UserService;

    @Test
    void findUserById(a){
        User user = UserService.findUserById(3); Assertions.assertNotNull(user); }}Copy the code

One of the simplest SpringBoot unit tests is to annotate the test class with @SpringBooTtest annotations, inject UserService with @Autowired, and assert the result with Assertions.

@SpringBootTest

This annotation creates an ApplicationContext to provide a context for the test, so in the example above we can use @autowired to inject UserService. The annotation provides several properties for the user to do some custom configuration, such as:

String[] propertiesandString[] value

Properties and value are aliases of each other. Make some configuration for the test environment. For example, set the web environment to reactive:

@SpringBootTest(properties = "spring.main.web-application-type=reactive") 
class MyWebFluxTests { 
    // ... 
}
Copy the code

String[] args

Introduce some parameters to the test program, such as:

@SpringBootTest(args = "--app.test=one")
class MyApplicationArgumentTests {

    @Test
    void applicationArgumentsPopulated(@Autowired ApplicationArguments args) {
        assertThat(args.getOptionNames()).containsOnly("app.test");
        assertThat(args.getOptionValues("app.test")).containsOnly("one"); }}Copy the code

Class<? >[] classes

A common way to inject a Test bean is to create a Test Spring Boot Boot class and inject it into the Test environment

SpringBootTest.WebEnvironment webEnvironment

Set the Test Web environment, which is an enumeration type, with the following parameters:

  • MOCK

This is the default option, which loads a Web ApplicationContext and provides a mock Web environment. The built-in container does not start.

  • RANDOM_PORT

Loads a WebServerApplicationContext and provide a real web environment, the built-in container will start and monitor a random port.

  • DEFINED_PORT

Loads a WebServerApplicationContext and provide a real web environment, the built-in container will start and monitor a custom port (8080 by default).

  • NONE

Loading an ApplicationContext does not provide any Web environment.

Note: Using the @Transactional annotation in the test can roll back transactions after the test is complete, but RANDOM_PORT and DEFINED_PORT provide a real Web environment that does not roll back transactions after the test is complete.

Layered testing and code testability

Layered testing

The above example is just a simple example. It is obvious that the test belongs to the service layer of a 3-tier architecture. What is layered testing?

Hierarchical testing, as the name suggests, is to write unit tests for each layer of the program. Although it takes more time to write unit tests, it can greatly ensure the stability of the code and locate bugs. If every test starts at the Controller layer, some underlying problems may be difficult to find. Therefore, it is recommended that you try to do hierarchical testing when writing unit tests.

Code testability

Code testability is simply how easy it is to write unit tests. If you find your code difficult to write unit tests, consider whether your code can still be optimized. Common test unfriendly code includes:

  • Abuse of mutable global variables
  • Abuse of static methods
  • Use complex inheritance relationships
  • Highly coupled code

Storage level test

In the case of Spring-data-JDBC, only the repository layer code is tested. The @datajdbctest annotation configes a built-in in-memory database and injects JdbcTemplate and Spring DataJdbc repositories. Other unnecessary components such as the Web layer are not introduced.

@DataJdbcTest
class UserMapperTest {

    @Autowired
    UserMapper userMapper;

    @Test
    public void test(a){
        userMapper.findById(1L) .ifPresent(System.out::println); }}Copy the code

Web layer test

@ @ WebMvcTest annotation automatically scanning Controller, @ ControllerAdvice, @ JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebM Inject MockMvc vcConfigurer and HandlerMethodArgumentResolver and automatically, we can use MockMvc test on our web.

Data:

insert into USERS(`username`, `password`)
values ('1', '111'),
       ('2', '222'),
       ('3', '333'),
       ('4', '444'),
       ('5', '555');
Copy the code

Controller layer code:

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

    final UserService userService;

    @GetMapping("/{id}")
    public User findById(@PathVariable long id){
        returnuserService.findUserById(id); }}Copy the code

Test code:

@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    MockMvc mvc;

    @MockBean
    UserService userService;

    @BeforeEach
    public void mock(a){
        when(userService.findUserById(anyLong())).thenReturn(User.builder().username("test").password("test").build());
    }

    @Test
    void exampleTest(a) throws Exception {
        mvc.perform(get("/users/1")) .andExpect(status().isOk()) .andDo(print()); }}Copy the code

After execution, the result is:

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"timestamp":0."status":0."message":null."data": {"id":0."username":"test"."password":"test"}}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
Copy the code

You can see that @webMvctest automatically injects MockMvc for us, but it doesn’t inject UserService, so we need to mock UserService, and in our mock() method we set up to return a Test object for any incoming Long, So we’re not going to get an object whose id is equal to 1.

Test the entire application

After the hierarchical testing is complete we may need to do a holistic test from top to bottom to verify the availability of the entire process, injecting the ApplicationContext of the entire test environment with @SpringBooTtest annotations and introducing MockMvc with @AutoConfiguRemockMVC.

The difference between @AutoconfiguRemockMVC and @webMvcTest is that @AutoConfiguRemockMVC simply infuses MockMvc while @WebMvcTest also introduces the ApplicationContext in the Web layer (note that it’s just w Eb layer context, which is why we need to mock other components.

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Test
    void exampleTest(@Autowired MockMvc mvc) throws Exception {
        mvc.perform(get("/users/1")) .andExpect(status().isOk()) .andDo(MockMvcResultHandlers.print()); }}Copy the code

Timestamp, status, message and so on are automatically added, so you can ignore them.

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json"]
     Content type = application/json
             Body = {"timestamp":0,"status":0,"message":null,"data":{"id":1,"username":"1","password":"111"}}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []
Copy the code

You can see that in the Body we get an object with id = 1, which means we got real data.