Skip to content

JUnit5 & MockMvc & Mockito

🏷️ Spring Boot Test JUnit

基于 spring-boot-start 2.2.1.RELEASEController 的测试类时,出了好多异常的状况。特将正常运行的代码记录下来,以防遗忘。

依赖版本

使用 Maven 管理的依赖。

  • JDK: 1.8
  • parentspring-boot-starter-parent 2.2.1.RELEASE
  • dependencies
    • spring-boot-starter-web 2.2.1.RELEASE
    • spring-boot-starter-test 2.2.1.RELEASE

测试类注意点

之所以踩了这么多坑,总结下来主要是因为网上的示例大都是基于不同的版本,从而导致写法也各不相同。将各种写法混在一起,又出现了各种奇怪的问题。
另外,我在公司的项目(依赖的包比较多)和我单独写的测试应用中(Spring 包的版本和公司项目是一致的),运行的结果也不一致。
最终的正确写法主要还是参考的官方文档,里面的说明比较详细和准确。

下面是总结的需要注意的地方:

  1. 2.2.1.RELEASE 版本中对应 JUnit 的版本是 5.5.2,测试类要使用 @SpringBootTest 注解;

    很多文档中使用了 @RunWith(SpringRunner.class) 注解,SpringRunnerJUnit4 中声明测试类用的。
    也不要添加 @ExtendWith({SpringExtension.class})@SpringBootTest 中已经包含了这个注解。

    java
    @SpringBootTest
    class UserControllerTest {
        // ...
    }
  2. 如果测试 Controller 接口需要添加 @AutoConfigureMockMvc 注解;

    使用 @SpringBootTest 注解,默认是不启动 Server 的。
    大多数示例中都是在 @Before 方法中通过 MockMvcBuilders 来创建 MockMvc 实例。
    另外在 JUnit5 中已经取消了 @Before 注解,改为使用 @BeforeEach 注解。

    java
    @AutoConfigureMockMvc
    @SpringBootTest
    class UserControllerTest {
        
        @Autowired
        private MockMvc mockMvc;
        
        @Test
        void login() throws Exception {
            ResultActions resultActions = this.mockMvc.perform(post("/user/login"));
            // 解决中文乱码问题
            resultActions.andReturn().getResponse().setCharacterEncoding("UTF-8");
            resultActions
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andExpect(MockMvcResultMatchers.content().string(containsString("OpenId B")))
                    .andDo(MockMvcResultHandlers.print());
        }
    }
  3. 如果需要 Mock 注入的服务等,需要添加 @ExtendWith(MockitoExtension.class) 注解,并且需要添加 @MockBean 在对应的字段(测试类中的字段)上;

    很多文档都是使用的 @Mock@IndectMocks 注解配合来实现 Mock 第三方服务的。

    java
    @AutoConfigureMockMvc
    @SpringBootTest
    @ExtendWith(MockitoExtension.class)
    class UserControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @MockBean
        private WxService wxService;
    
        @Test
        void login() throws Exception {
            WxLoginResponse wxLoginResponse = new WxLoginResponse();
            wxLoginResponse.setOpenId("假用户 OpenId B");
    
            Mockito.when(wxService.login(any(), any(), any(), any()))
                    .thenReturn(wxLoginResponse);
    
            // ...
        }
    }

测试中曾经出现过的问题:

  1. 由于 Controller 中使用了 @RequiredArgsConstructor 注解,导致 Mock 始终不起作用;
    这个最终实现了使用 @RequiredArgsConstructor 注解时仍然可以正常 Mock 。不过当时确实通过改为使用 @Autowired 注解实现了 Mock ,不过随之而来的是 Mock 的接口返回值始终是 null 的问题。
  2. Mock 的服务返回的结果始终是 null
    这个在正式项目和测试项目中运行的结果经常不一致;测试项目中大部分写法都是可以正常返回的,但是正式项目中,不知道什么原因,一致返回的是 null
    只有通过 this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build(); 才能注入 Mock 的服务,但最后发现在 JUnit5 是不需要手动创建 mockMvc 实例的,可以简单的通过添加 @Autowired 注解来注入。
  3. Controller 中除了 Mock 的服务,其它需要注入的组件全都是 null
    查到的很多文档中都是通过在被 Mock 的服务字段上添加 @Mock 注解,在使用 Mock 服务的组件上添加 @InjectMocks 注解。
    但是在 JUnit5 中只需要在被 Mock 的服务上添加 @MockBean 注解,需要在测试类中定义使用 Mock 服务的组件字段。
  4. 接口调用时,Filter 没有执行;
    之前是通过在 mockMvcbuilder 处理中添加 .addFilter(signAuthFilter) 来实现的,但是之后使用 @AutoConfigureMockMvc 注解来注入 mockMvc 实例后,就不需要手动添加 filter 了。

附 1. 完整示例代码

1. pom.xml

xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.1.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>me.liujiajia</groupId>
	<artifactId>junit5-sample</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>junit5-sample</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

2. Junit5SampleApplication.java

java
package me.liujiajia.junit5sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Junit5SampleApplication {

	public static void main(String[] args) {
		SpringApplication.run(Junit5SampleApplication.class, args);
	}

}

3. UserController.java & WxLoginResponse.java

java
package me.liujiajia.junit5sample.controller;

import lombok.extern.slf4j.Slf4j;
import me.liujiajia.junit5sample.entity.WxLoginResponse;
import me.liujiajia.junit5sample.service.AnotherService;
import me.liujiajia.junit5sample.service.WxService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by 佳佳 on 2021/4/26.
 */
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private WxService wxService;

    @Autowired
    private AnotherService anotherService;

    @PostMapping("login")
    public WxLoginResponse login() {
        WxLoginResponse wxLoginResponse = wxService.login("appId", "secret", "code", "clientCredential");
        return wxLoginResponse;
    }
}
java
package me.liujiajia.junit5sample.entity;

/**
 * Created by 佳佳 on 2021/4/26.
 */
public class WxLoginResponse {
    private String openId;

    public String getOpenId() {
        return openId;
    }

    public void setOpenId(String openId) {
        this.openId = openId;
    }
}

4. WxService.java & WxServiceImpl.java

java
package me.liujiajia.junit5sample.service;

import me.liujiajia.junit5sample.entity.WxLoginResponse;

/**
 * Created by 佳佳 on 2021/4/26.
 */
public interface WxService {
    WxLoginResponse login(String appId, String secret, String code, String clientCredential);
}
java
package me.liujiajia.junit5sample.service.impl;

import me.liujiajia.junit5sample.entity.WxLoginResponse;
import me.liujiajia.junit5sample.service.WxService;
import org.springframework.stereotype.Service;

/**
 * Created by 佳佳 on 2021/4/26.
 */
@Service
public class WxServiceImpl implements WxService {
    @Override
    public WxLoginResponse login(String appId, String secret, String code, String clientCredential) {
        WxLoginResponse wxLoginResponse = new WxLoginResponse();
        wxLoginResponse.setOpenId("OpenId A");
        return wxLoginResponse;
    }
}

5. AnotherService.java & AnotherServiceImpl.java

这个是为了验证之前遇到的服务为空的问题。

java
package me.liujiajia.junit5sample.service;

public interface AnotherService {
}
java
package me.liujiajia.junit5sample.service.impl;

import me.liujiajia.junit5sample.service.AnotherService;
import org.springframework.stereotype.Service;

@Service
public class AnotherServiceImpl implements AnotherService {
}

6. UserControllerTest.java

java
package me.liujiajia.junit5sample;

import me.liujiajia.junit5sample.entity.WxLoginResponse;
import me.liujiajia.junit5sample.service.WxService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.ArgumentMatchers.any;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

/**
 * Created by 佳佳 on 2021/4/26.
 */
@AutoConfigureMockMvc
@SpringBootTest
@ExtendWith(MockitoExtension.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private WxService wxService;

    @Test
    void login() throws Exception {
        WxLoginResponse wxLoginResponse = new WxLoginResponse();
        wxLoginResponse.setOpenId("假用户 OpenId B");

        Mockito.when(wxService.login(any(), any(), any(), any()))
                .thenReturn(wxLoginResponse);

        ResultActions resultActions = this.mockMvc.perform(post("/user/login"));
        // 解决中文乱码问题
        resultActions.andReturn().getResponse().setCharacterEncoding("UTF-8");
        resultActions
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string(containsString("OpenId B")))
                .andDo(MockMvcResultHandlers.print());
    }
}

附 2. 参考文档

  1. Mockito - NullpointerException when stubbing Method
  2. Testing MVC Web Controllers with Spring Boot and @WebMvcTest
  3. 使用 MockMvc 与 SpringBootTest 和使用 WebMvcTest 之间的区别
  4. Spring Boot Reference Documentation