基础
基础概念
定义:
是Spring框架提供的一种用于测试Spring MVC控制器的工具,它允许开发者在不启动完整的web服务器的情况下,模拟HTTP请求并验证响应。
优点:
执行速度快 --》 不需要启动web服务器;
便于集成 --》 可以与Junit、TestNG等测试框架无缝衔接;
强大的功能 --》 对HTTP请求的详细配置和响应的全面验证;
依赖配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Mockxxx
MockMvc:
来自于:
import org.springframework.test.web.servlet.MockMvc;
定义:
是Spring Test模块的一部分,它允许开发者对Spring MVC控制器进行单元测试而无需启动完整的Web服务器;通过MockMvc,可以模拟HTTP请求并验证响应,使得测试执行速度更快,同时便于与JUnit、TestNG等测试框架集成。
使用:
使用mockMvc。perform()模拟HTTP请求,使用.andExpect()和.andReturn()等方法进行响应验证。
Mockito:
来自于:
import org.mockito.Mockito;
定义:
流行的Java单元测试框架,专门用于创建和验证模拟对象的行为。它允许开发者在编写测试时模拟外部依赖,从而使得测试更便捷,减少对外部类、系统和依赖给单元测试带来的耦合。
特点:
行为验证、测试桩、参数匹配器、注册支持、监控真实对象、重置mock对象;
MvcResult:
来自于:
import org.springframework.test.web.servlet.MvcResult;
定义:
是在执行模拟HTTP请求以测试控制器(Controller)层功能时的重要概念,它代表一个完整的HTTP响应结果,包括响应的状态码、头信息、响应体以及任何可能产生的错误等。
作用:
封装HTTP响应 -》封装由模拟的HTTP产生的完整响应信息;
获取响应细节 -》包括响应的状态码、头信息、响应体等内容;
断言测试 -》 使用MvcResult中的方法进行断言,确保控制器返回正确的HTTP状态码和数据;
文件位置
与项目目录保持一致;
假设项目文件为
src/main/java/xxx/controller/xxxController
测试类项目文件为
src/test/java/xxx/controller/xxxTest
注解
🍎@RunWith
🍇@RunWith(MockitoJUnitRunner.class)
是 JUnit 测试框架中的一个注解,用于指定测试运行器(Test Runner)。
它的作用是将 Mockito 框架与 JUnit 集成起来,从而简化 Mock 对象的创建和管理。
案例:
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
@Mock
private MyDependency mockDependency;
@InjectMocks
private MyService myService;
@Test
public void testMethod() {
when(mockDependency.someMethod()).thenReturn("Mocked Value");
String result = myService.callDependency();
assertEquals("Mocked Value", result);
}
}
🍇@RunWith(PowerMockRunner.class)
是 PowerMock 框架提供的一个测试运行器,用于扩展 Mockito 或其他 Mock 框架的功能。
PowerMock 支持更高级的 Mock 能力,例如对静态方法、私有方法、构造函数等进行 Mock。
案例:
@RunWith(PowerMockRunner.class)
@PrepareForTest(MyClassWithStaticMethod.class) // 指定需要 Mock 的类
public class MyStaticMethodTest {
@Test
public void testStaticMethod() throws Exception {
PowerMockito.mockStatic(MyClassWithStaticMethod.class);
when(MyClassWithStaticMethod.staticMethod()).thenReturn("Mocked Static Value");
String result = MyClassWithStaticMethod.staticMethod();
assertEquals("Mocked Static Value", result);
}
}
🍇@RunWith(SpringRunner.class)
用于 Spring 集成测试,结合 Spring TestContext 框架加载 Spring 上下文。
加载 Spring 配置文件或注解配置。
支持依赖注入(@Autowired)。
支持事务管理(@Transactional)。
案例:
@RunWith(SpringRunner.class)
@SpringBootTest
public class MySpringBootTest {
@Autowired
private MyService myService;
@Test
public void testService() {
String result = myService.performAction();
assertNotNull(result);
}
}
🍇@RunWith(Parameterized.class)
用于参数化测试,允许为同一个测试方法提供多组输入数据。
案例:
@RunWith(Parameterized.class)
public class ParameterizedTest {
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ 1, 2, 3 },
{ 4, 5, 9 },
{ 6, 7, 13 }
});
}
private int a;
private int b;
private int expected;
public ParameterizedTest(int a, int b, int expected) {
this.a = a;
this.b = b;
this.expected = expected;
}
@Test
public void testAddition() {
assertEquals(expected, a + b);
}
}
🍇@RunWith(Cucumber.class)
用于行为驱动开发(BDD)测试,结合 Cucumber 框架编写基于 Gherkin 语言的测试用例。
解析 .feature 文件中的测试步骤。
将自然语言描述的测试用例映射到 Java 方法。
案例:
@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources/features")
public class CucumberTest {
}
🍇@RunWith(JUnitParamsRunner.class)
类似于 Parameterized,但提供了更灵活的参数化测试方式。
支持多种数据源(数组、文件、数据库等)。
更简洁的语法。
案例:
@RunWith(JUnitParamsRunner.class)
public class JUnitParamsTest {
@Test
@Parameters({ "1, 2, 3", "4, 5, 9", "6, 7, 13" })
public void testAddition(int a, int b, int expected) {
assertEquals(expected, a + b);
}
}
🍎@Mock
作用 :
创建一个完全模拟对象(mock object)。
特点 :
使用 @Mock 注解的对象是一个“假”的对象,它的所有方法默认不会执行真实逻辑,而是返回默认值(例如,null、0 或空集合等)。
可以通过 when(...).thenReturn(...) 或其他方式显式定义其行为。
适用于完全隔离被测类的依赖。
🍎@Spy
作用 :
创建一个部分模拟对象(partial mock),即对真实对象的部分方法进行模拟。
特点 :
使用 @Spy 注解的对象是一个真实的实例,但可以对其某些方法进行模拟(stubbing)。
如果没有显式定义某个方法的行为,则会调用该方法的真实实现。
适用于需要保留某些方法的实际逻辑,同时对其他方法进行模拟的场景。
🍎@InjectMocks
作用 :
自动将 @Mock 或 @Spy 标记的依赖注入到被测类中。
特点 :
使用 @InjectMocks 注解的类是被测类(通常是业务逻辑类)。
Mockito 会自动将标记为 @Mock 或 @Spy 的依赖注入到被测类的字段中(支持构造函数注入、setter 方法注入或字段注入)。
适用于简化依赖注入的过程。
🍎@SneakyThrows
定义:
是 Lombok 提供的一个注解,旨在帮助开发者简化异常处理。它允许方法抛出检查型异常而无需显式声明或捕获这些异常。
会将检查型异常转换为运行时异常;
使用:
该注解替代到try..catch 以及 throws Exception等执行;
存在问题:
异常处理的不明确性;
`@SneakyThrows` 将检查型异常转换为运行时异常,调试过程中可能难以追踪异常的来源和具体类型。
一般实现
// 类注解,用于配置 JUnit 5 测试类以使用 Spring 的测试支持。
@ExtendWith(SpringExtension.class)
// 是一个 Spring Boot 提供的注解,用于对 Web 层(即控制器层)进行测试。如果使用该注解,那么Spring Boot 会自动配置一个模拟的 Spring MVC 环境,这样就可以在不启动完整应用的情况下测试控制器的行为。
@WebMvcTest(value = {Controller.class, Handler.class})
// 完成bean自动装配
@Autowired
private MockMvc mockMvc;
// 标识测试方法
@Test
MockHttpServletRequestBuilder requestBuilder = get("控制类路径/xxx/xxx");
// MockHttpServletRequestBuilder 是一个用于构建模拟HTTP请求的工具类。
// 主要在单元测试和集成测试中使用,构造出特定的HTTP请求来测试控制器(Controller)或端点(Endpoint)。
requestBuilder.param("参数名", 参数值);
// 给已创建的mockhttpservletrequestbuilder实例添加查询参数;
// 将参数通过键值对的形式填入MockHttpServletRequestBuilder对象中;
mockMvc.perform(requestBuilder) // mockMvc对象执行,之前定义的requestbuiler对象
.andExpect(status().isOk()) // 该调用方式是链式调用;--》 用于检查HTTP响应的状态码是否是200
.andDo(new ResultHandler() { // 自定义处理MvcResult对象,即模拟请求后的结果
@Override
public void handle(MvcResult mvcResult) throws Exception {
// 获取响应体并将其转化为字符串类型;
String content = mvcResult.getResponse().getContentAsString();
// 检查响应体是否为空,以确保有数据返回;
assertTrue(StringUtils.isNotBlank(content));
// 将响应体内容解析成map集合
Map<String, String> resp = JSONUtils.toMap(content);
// 验证code/message 两个属性的值是否等于1/success;
assertEquals("1", resp.get("code"));
assertEquals("success", resp.get("message"));
// 获取data属性对应的值
String data = resp.get("data");
// base64解码
byte[] encryptedData = Base64.decode(data);
// aes密钥生成
AES aes = genAES(keyIv);
// 解密data对应的值
String json = new String(aes.decrypt(encryptedData), StandardCharsets.UTF_8);
// json format is ResponseKeyCollection 自定义的一个类
ResponseKeyCollection collection = JSONUtils.toObject(json, ResponseKeyCollection.class);
// 将json转成 ResponseKeyCollection对象
assertEquals(10, collection.getResponseKeys().size());
// 验证对象中getResponseKeys方法返回的集合大小是否为10
}
});
@BeforeEach
ResponseKey responseKey1 = COLLECTION.getResponseKeys().stream().filter(key -> key.getIndex() == 1).findFirst().orElse(null);
// COLLECTION获取ResponseKeys属性--》转换为流--》过滤出index=1的responseKey对象
// --》查找第一个满足条件的--》有则返回,无则返回null;
// COLLECTION 是自定义的对象
Mockito.when(keyStoreService.getResponseKey(1)).thenReturn(responseKey1);
// 模拟该方法,当此入参为1时,则返回上述找到的对象;
Mockito.when(keyStoreService.generateResponseKeyCollection(10)).thenReturn(COLLECTION);
// 模拟该方法,当此入参为10时,则返回Collecion对象;
Mockito.when(keyHelper.isExpiredKeyIndex(anyInt())).thenReturn(false);
// 模拟此方法,不管该方法入参为谁,都返回false --》 即keyIndex永不过期
测试注意事项
🐱 注意空指针异常,即对象.getXxx()方法执行时,对象为null;
🐱 使用switch..cace时,要注意添加default;
实战
MockMvc与Test注解不兼容
参考博客:https://segmentfault.com/q/1010000042943340
描述:
使用
@Autowired
private MockMvc mockMvc;
总是导致注入的mockMvc失败;
原因是:
MockMvc与@Test不兼容的问题;
原本依赖库为:org.junit.Test
改成org.junit.jupiter.api.Test 就可以了;
两个依赖库之间的关系为:
org.junit.Test --》 JUnit4
org.junit.jupiter.api.Test --》 JUnit5
@RequestParams参数
MockHttpServletRequestBuilder requestBuilder = get("/decrypt");
requestBuilder.param("id1", id1);
requestBuilder.param("id2", id2);
mockMvc.perform(requestBuilder)
.andExpect(status().isOk())
.andDo(mvcResult -> {
String content = mvcResult.getResponse().getContentAsString();
});
@RequestBody参数
接口请求参数:
@RequestMapping("/getfunction1")
public ResultDtoRisk riskGetTokenByph(HttpServletRequest request, @RequestBody NumReq numReq){}
测试代码:
@Autowired
pivate MockMvc mockMvc;
// import org.springframework.test.web.servlet.MockMvc;
NumReq numReq = new NumReq();
numReq.setId1(id1);
numReq.setId2(id2);
MockHttpServletRequestBuilder requestBuilder = get("/getfunction1");
// import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
String requestBodyContent = objectMapper.writeValueAsString(numReq);
requestBuilder.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(requestBodyContent);
mockMvc.perform(requestBuilder)
.andExpect(status().isOk())
.andDo(print());
when()
问题描述:
org.mockito.exceptions.misusing.MissingMethodInvocationException:
when() requires an argument which has to be 'a method call on a mock'.
For example:
when(mock.getArticles()).thenReturn(articles);
Also, this error might show up because:
you stub either of: final/private/equals()/hashCode() methods.
Those methods cannot be stubbed/verified.
Mocking methods declared on non-public parent classes is not supported.
inside when() you don't call method on mock but on some other object.
分析:
when() 的参数必须是一个对 Mock 对象的方法调用 :
when() 方法的作用是模拟某个 Mock 对象的行为(例如返回值或抛出异常)。
如果传入的参数不是对 Mock 对象的方法调用,而是普通对象或其他操作,就会抛出该异常。
不能对 final、private、equals() 或 hashCode() 方法进行 Mock
解决:
添加代码:Mockito.mockStatic(xxx.class) 其中xxx是使用到的静态对象;
@Mock
问题描述:
org.mockito.exceptions.misusing.WrongTypeOfReturnValue:
MsGatewayAddressVo cannot be returned by getUpdateTime()
getUpdateTime() should return LocalDateTime
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
This exception might occur in wrongly written multi-threaded tests.
Please refer to Mockito FAQ on limitations of concurrency testing.
A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies -
with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.
分析:
when() 的参数必须是对 Mock 对象的方法调用 :
在你的代码中,when(MapperConvert.FACADE.map(msGatewayAddress, MsGatewayAddressVo.class)) 可能存在问题。
如果 MapperConvert.FACADE 不是一个 Mock 对象,而是普通对象,Mockito 无法对其进行 Stubbing,从而抛出异常。
故此可以将MapperConvert.FACADE 定义成Mock对象;
但MapperConvert.FACADE是类的静态属性,Mockito 默认不支持对静态成员进行 Mock。
故此不能采用上述形式;
添加Mockito-inline 是 Mockito 的扩展库,支持对静态方法和静态成员的 Mock;
引入Mockito-inline,MockedStatic爆红:
原因 = Mockito-inline 和 mockito-core 版本不匹配;全部改用5.2.0版本
报错:
java: 无法访问org.mockito.InjectMocks
错误的类文件: /D:/soft/maven/responsitory/org/mockito/mockito-core/5.2.0/mockito-core-5.2.0.jar!/org/mockito/InjectMocks.class
类文件具有错误的版本 55.0, 应为 52.0
改成4.11.0 上述问题没有了
但又出现了新的问题:
java.lang.IllegalStateException: Could not initialize plugin: interface org.mockito.plugins.MockMaker (alternate: null)
Caused by: org.mockito.exceptions.base.MockitoInitializationException:
It seems like you are running Mockito with an incomplete or inconsistent class path. Byte Buddy could not be loaded.
Byte Buddy is available on Maven Central as 'net.bytebuddy:byte-buddy' with the module name 'net.bytebuddy'.
For the inline mock maker, 'net.bytebuddy:byte-buddy-agent' with the module name 'net.bytebuddy.agent' is also required.
Normally, your IDE or build tool (such as Maven or Gradle) should take care of your class path completion but
添加依赖:
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.15.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.15.11</version>
<scope>test</scope>
</dependency>
又出现新问题:
org.mockito.exceptions.misusing.WrongTypeOfReturnValue:
MsGatewayAddressVo cannot be returned by getUpdateTime()
getUpdateTime() should return LocalDateTime
***
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. This exception *might* occur in wrongly written multi-threaded tests.
Please refer to Mockito FAQ on limitations of concurrency testing.
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies -
- with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.
将when(xxxInfo.get(createTime)).thenReturn(createTime);
改成:doReturn(createTime).when(xxxInfo).getCreateTime();
can not find lambda cache for this entity
描述:
通过mock模拟这段代码 LambdaQueryWrapper<MsJtipInfo> mjii = Wrappers.lambdaQuery(); mjii.select(MsJtipInfo::getIpStart, MsJtipInfo::getEndNum,MsJtipInfo::getStartNum, MsJtipInfo::getStatus,MsJtipInfo::getProvId,MsJtipInfo::getProvName); 出现com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: can not find lambda cache for this entity
分析:
MyBatis-Plus 的 Lambda 表达式功能强大,允许开发者通过类似于 Java 8 的语法查询数据库。这背后涉及到通过 Lambda 表达式获取实体类的字段映射信息。TableInfoHelper 类是核心,它通过扫描实体类元数据并建立缓存来实现数据库字段与实体类字段之间的映射。通常在运行时环境下,这个过程是自动触发的,然而在单元测试时,自动化程度较低,导致缓存未能及时初始化。
LambdaQueryWrapper<xxxInfo> mjii = Wrappers.lambdaQuery();
mjii.select(xxxInfo::getIpStart);
MyBatis-Plus 会尝试通过 xxxInfo::getIpStart获取 xxxInfo 实体的字段名。这一操作依赖于 TableInfoHelper 的缓存,如果没有缓存,Lambda 表达式解析就会失败,进而导致抛出 can not find lambda cache for this entity 异常。
解决:
@Before
// junit4
// @BeforeEach junit5
void setUp() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), xxxInfo.class);
}
when-thenReturn和doReturn-when区别
when().thenReturn()
用法实例:when(mock.method()).thenReturn(value)
是否执行真实方法:执行真实方法(对 Spy 对象而言)
适用方法类型:非 void 方法
异常处理:若真实方法抛异常,需先处理
链式调用:支持 .thenReturn().thenThrow()
doReturn().when()
用法实例:doReturn(value).when(mock).method()
是否执行真实方法:不执行真实方法(直接覆盖行为)
适用方法类型:任意方法(包括 void 方法)
异常处理:安全跳过真实方法执行,避免异常
链式调用:需通过 doReturn().doThrow().when() 链式调用
mock对象和new对象
🌟 Mock对象 vs 普通对象 对比表 🌟
📌 核心用途
Mock对象 ✨
用于模拟依赖项,隔离被测试代码与其他组件的交互。
🛠️ 适用场景:测试依赖外部服务(如数据库/API)的类,避免真实调用。
// 示例:模拟数据库调用
when(mockDao.query()).thenReturn(fakeData);
普通对象 ✨
用于测试类本身的逻辑,适合无复杂依赖的场景。
🛠️ 适用场景:测试简单类(如DTO/POJO),直接验证属性或方法。
UserDto user = new UserDto("Alice", 25);
assertEquals("Alice", user.getName());
🛠️ 创建方式
Mock对象 🎨
通过 Mockito.mock() 创建,需依赖Mockito库。
MsPackageInfo mock = Mockito.mock(MsPackageInfo.class);
普通对象 🎨
直接通过 new 关键字创建,无需额外依赖。
UserDto user = new UserDto();
🌈 方法调用
Mock对象 🎭
通过 when().thenReturn() 定义方法返回值。
when(mock.getStatus()).thenReturn(StatusEnum.ACTIVE);
普通对象 🎭
执行真实方法,依赖类的实际实现逻辑。
int result = calculator.add(1, 2); // 真实计算
🎨 属性设置
Mock对象 🖍️
通过方法返回值模拟属性,不直接操作属性。
when(mock.getUserName()).thenReturn("MockUser");
普通对象 🖍️
直接通过构造函数或Setter设置属性。
user.setName("RealUser"); // 直接赋值
🚀 典型场景
Mock对象 🛡️
测试服务类时,模拟数据库/外部接口调用。
when(userService.getUserById("123")).thenReturn(mockUser);
普通对象 📦
测试数据对象(如DTO)的属性赋值和Getter/Setter。
assertEquals("Alice", user.getName()); // 验证属性
🎉 一句话总结 🎉
Mock对象 = 测试中的“替身演员” 🎬,隔离依赖!
普通对象 = 测试中的“真实主角” 🌟,直接验证逻辑!
BeforeEach出现的内容
MockitoAnnotations.initMocks(this);
// 初始化测试类中的 @Mock 和 @InjectMocks 注解。
// 创建模拟对象(mock)并将其注入到被测试的对象中
mockMvc = MockMvcBuilders.standaloneSetup(xxxService).build();
// 构建一个独立的 MockMvc 实例;
// 直接测试传入的服务或控制器,而无需启动完整的 Spring 容器。
同类测试方法a调用方法b
🍇 代码
public class ClassA{
public Result functionA(){
functionB();
if(xxxx){
return new Result();
}esle{
return functionC();
}
}
public void functionB(){}
public Result functionC(){
xxx;
return new Result();
}
}
@Data
public class Result{
private String code;
}
🍇 Mock模拟
public void ClassATest{
@Spy
@InjeckMocks
private ClassA classA;
@BeanforeEach
public void setUp(){
// 初始化@Mock 和 @InjectMocks 修饰的对象
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(classA).build();
🍑 方法1
// 将模拟functionA所需要调用的functionB、C过程,在setUp中实现;
doNothing().when(classA).functionB();
Result re = new Result();
re.setCode("testCode");
doReturn(re).when(classA).functionC();
}
@Test
public void testFunctionA(){
🍑 方法2:
// 将模拟functionA所需要调用的functionB、C过程,在setUp中实现;
doNothing().when(classA).functionB();
Result re = new Result();
re.setCode("testCode");
doReturn(re).when(classA).functionC();
Result result = classA.functionA();
assert(result.getCode(),"testCode");
}
}
// 方法1、2 存在一个即可;
原始值与参数匹配器混用
报错信息:
This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(any(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
//correct:
someMethod(any(), eq("String by matcher"));
案例:
someMethod(any(), null); // 错误案例
someMethod(any(), isNull()); // 正确案例;
Mocktio严格模式(strict stubbing)
🍎 案例1
描述:
org.mockito.exceptions.misusing.PotentialStubbingProblem:
Strict stubbing argument mismatch. Please check:
- this invocation of 'decryptRequestBody' method: // 真实返回方法的入参
thrOperatorSwapAbstractService.decryptRequestBody(
null,
"2.0",
"testApiKey",
""
);
- has following stubbing(s) with different arguments: // mock模拟的入参
1. thrOperatorSwapAbstractService.decryptRequestBody(
null,
null,
null,
null
);
mock模拟:
decryptRequestBody(any(String.class),any(String.class),any(String.class),any(String.class))
虽然模拟显示全是null,但是可以匹配到对应的参数 即参数2、3、4
其中参数1没有匹配成功;真实请求返回的是null;
也就是通过any(String.class) 模拟会产生null,但是如果真实请求参数有null的话 是不会和any(String.class)匹配的;
void方法mock
正常执行 :使用 doNothing()。
抛出异常 :使用 doThrow()。
执行自定义逻辑 :使用 doAnswer()。
多次调用不同行为 :可以链式调用 doNothing()、doThrow() 等。
案例1:
Mockito.doNothing().when(xxx.class);
xxx.xxx(any(xxx.class), anyInt(), any(xxx.class), any(xxx.class));
xxx.xxx是静态方法调用;
静态方法mock
🍇 方法: class.StaticFunction.get()
其中 class.StaticFunction返回结果是Map集合;
🍇 mock
Map<String,String> map = mock(HashMap.class);
when(class.getStaticFunction()).thenReturn(map);
when(map.get(anyString())).thenReturn();