文章目录
- 单元测试Mockito
- [1. 入门](#1. 入门)
-
- [1.1 什么是Mockito](#1.1 什么是Mockito)
- [1.2 优势](#1.2 优势)
- [1.3 原理](#1.3 原理)
- [2. 使用](#2. 使用)
-
- [2.0 环境准备](#2.0 环境准备)
- [2.1 Mock](#2.1 Mock)
-
- 1) Mock对象创建 Mock对象创建)
- 2) 配置Mock对象的行为(打桩) 配置Mock对象的行为(打桩))
- 3) 验证方法调用 验证方法调用)
- 4) 参数匹配 参数匹配)
- 5) 静态方法 静态方法)
- [2.2 常用注解](#2.2 常用注解)
-
- 1) @Mock @Mock)
- 2) @BeforeEach 与 @BeforeAfter @BeforeEach 与 @BeforeAfter)
- 3) @InjectMocks @InjectMocks)
- 4) @Spy @Spy)
- 5) @Captor @Captor)
- 6) @RunWith和@ExtendWith @RunWith和@ExtendWith)
- [2.3 常见区别](#2.3 常见区别)
- [3. Springboot 使用](#3. Springboot 使用)
-
- [3.1 数据准备](#3.1 数据准备)
- [3.2 测试](#3.2 测试)
- [3.3 Springboot测试注解](#3.3 Springboot测试注解)
单元测试Mockito
名称 | 链接 | 备注 |
---|---|---|
mockito英文文档 | Mockito (Mockito 5.12.0 API) (javadoc.io) | |
mockito中文文档 | Mockito 中文文档 ( 2.0.26 beta ) - 《Mockito 框架中文文档》 - 极客文档 (geekdaxue.co) | |
视频教学链接 | https://www.bilibili.com/video/BV1P14y1k7Hi |
1. 入门
1.1 什么是Mockito
Mockito是Java生态系统中最受欢迎的单元测试模拟框架之一,以其简洁易用的API和强大的模拟能力赢得了广大开发者的青睐。Mockito允许我们在不实际依赖外部资源的情况下对代码进行彻底且高效的单元测试,极大地提升了测试覆盖率和代码质量。
1.2 优势
Mockito是一种模拟框架,其核心概念是在测试过程中创建并使用"Mock对象"。Mock对象是对实际对象的一种模拟,它继承或实现了被测试类所依赖的接口或类,但其行为可以根据测试需求自由定制。控制其在测试环境下的行为,从而将注意力聚焦于类本身的逻辑验证上。
- 隔离度高:通过模拟依赖,减少测试间的耦合,确保单元测试真正只关注被测试单元的内部逻辑。
- 易于使用:API设计直观简洁,降低了编写和阅读测试用例的难度。
- 详尽的验证:能够准确跟踪和验证被测试对象与其依赖之间的交互行为。
- 灵活性强:支持多种定制模拟行为,无论是简单的返回值还是复杂的回调机制。
- 有利于TDD实践:与测试驱动开发方法论紧密契合,鼓励写出更易于测试的代码。
1.3 原理
Mockito
的底层原理是使用 cglib
动态生成一个 代理类对象 ,因此,mock
出来的对象其实质就是一个 代理 ,该代理在 没有配置/指定行为 的情况下,默认返回空值
2. 使用
2.0 环境准备
创建一个普通的maven项目。添加依赖
xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ucarinc.framework</groupId>
<artifactId>demo1</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo1</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
2.1 Mock
1) Mock对象创建
使用Mockito.mock()
方法创建接口或抽象类的Mock对象。下面是它的方法接口
java
public static <T> T mock(Class<T> classToMock)
- classToMock:待 mock 对象的 class 类。
- 返回 mock 出来的类
实例:使用 mock 方法 mock 一个类
java
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import static org.mockito.Mockito.*;
public class MyTest {
@Test
public void myTest() {
/* 创建 Mock 对象 */
List list = mock(List.class);
/* 设置预期,当调用 get(0) 方法时返回 "111" */
when(list.get(0)).thenReturn("111");
Assert.assertEquals("asd", 1, 1);
/* 设置后返回期望的结果 */
System.out.println(list.get(0));
/* 没有设置则返回 null */
System.out.println(list.get(1));
/* 对 Mock 对象设置无效 */
list.add("12");
list.add("123");
/* 返回之前设置的结果 */
System.out.println(list.get(0));
/* 返回 null */
System.out.println(list.get(1));
/* size 大小为 0 */
System.out.println(list.size());
/* 验证操作,验证 get(0) 调用了 2 次 */
verify(list, times(2)).get(0);
/* 验证返回结果 */
String ret = (String)list.get(0);
Assert.assertEquals(ret, "111");
}
}
总结
junit4 | junit5 | |
---|---|---|
方法一 | @RunWith(MockitojUnitRunner.class)+@Mock等注解 | @ExtendWith(MockitoExtension.class)+@Mock等注解 |
方法二 | Mockito.mock(X.class)MockitoAnnotations.open等静态方法 | Mockito.mock(X.class)MockitoAnnotations.open等静态方法 |
方法三 | Mocks(this)+@Mock等注解 | Mocks(this)+@Mock等注解 |
2) 配置Mock对象的行为(打桩)
使用when
和thenReturn
方法配置Mock对象的行为:
打桩可以理解为mock
对象规定一行的行为,使其按照我们的要求来执行具体的操作。在Mockito
中,常用的打桩方法为
方法 | 含义 |
---|---|
when().thenReturn() | Mock 对象在触发指定行为后返回指定值 |
when().thenThrow() | Mock 对象在触发指定行为后抛出指定异常 |
when().doCallRealMethod() | Mock 对象在触发指定行为后调用真实的方法 |
thenReturn() 代码示例
java
public void test02(){
// 模拟random对象,这个对象是假的
Random random = Mockito.mock(Random.class);
// 当调用了random对象时,返回100这个值
Mockito.when(random.nextInt()).thenReturn(100);
// 验证,应该是对的。有人会问,random.nextInt()不是获取随机值吗?
// 现在这个random对象是假的
Assertions.assertEquals(100, random.nextInt());
}
完整的另一个demo
java
package com.ucarinc.framework;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class App5Test {
private final Logger log= LoggerFactory.getLogger(App5Test.class);
public static class MockitoTestController{
public int add(int a, int b){
System.out.println("测试了a+b a="+a+",b="+b);
return a+b;
}
}
@Test
void testAdd() {
MockitoTestController mockitoTestController = mock(MockitoTestController.class);
// 设置mock对象的行为(打桩),当调用add(1, 2)时返回4
when(mockitoTestController.add(1, 2)).thenReturn(4);
// 调用mock对象的方法,返回为4
int result = mockitoTestController.add(1, 2);
log.info("mockitoTestController.add result={}",result);
// 断言验证:调用add(1, 2)方法返回值是否为4
Assertions.assertEquals(mockitoTestController.add(1, 2),4);
// 验证:确保add方法(1, 2)被调用了一次
verify(mockitoTestController,times(2)).add(1, 2);
}
}
你还可以配置方法抛出异常:
java
/**
* 测试当调用add方法时抛出RuntimeException异常的情况。
* 该测试函数不接受参数,也没有返回值。
*/
@Test
void testAddException() {
TestController mockitoTestController = Mockito.mock(TestController.class);
// 设置mock对象,在调用mockitoTestController的add方法时抛出RuntimeException异常
when(mockitoTestController.add(1, 2)).thenThrow(new RuntimeException("add error"));
// 验证是否抛出了RuntimeException异常
Assertions.assertThrows(RuntimeException.class, () -> mockitoTestController.add(1, 2));
}
public static class TestController{
public int add(int a, int b){
System.out.println("测试了a+b="+a+",b="+b);
return a+b;
}
}
有种特殊情况,就是void返回值打桩
java
package com.lkcoffee.framework.demo2;
/**
* @Desciption:
* @Author: feixiang.li
* @date: 2024-07-12 14:38
**/
import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
public class Test4 {
@Mock
List<String> mockList;
@Test
public void test1(){
doNothing().when(mockList).clear();
mockList.clear();
verify(mockList).clear();
}
}
3) 验证方法调用
Mock对象进行行为验证和结果断言。验证是校验对象是否发生过某些行为,Mockito 中验证的方法是:verify
。
常见的验证方法包括:
verify(mock).methodCall()
:验证方法被调用verify(mock, times(n)).methodCall()
:验证方法被调用n次verify(mock, never()).methodCall()
:验证方法从未被调用
验证交换 :Verify 配合 time() 方法,可以校验某些操作发生的次数。
注意:当使用 mock 对象时,如果不对其行为进行定义,则 mock 对象方法的返回值为返回类型的默认值。
java
package com.ucarinc.framework;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Random;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class AppTest{
@Test
public void test01() {
// 使用Mockito模拟一个Random对象
Random random = Mockito.mock(Random.class);
// 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)
System.out.println("第一次:"+random.nextInt());
// 验证random.nextInt()这个方法是否只调用了一次
verify(random).nextInt();
// 指定当调用nextInt()时,始终返回1
Mockito.when(random.nextInt()).thenReturn(1);
System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1
// 断言nextInt()方法返回值是否为1
Assertions.assertEquals(1,random.nextInt());
// 验证nextInt()方法是否被调用了两次
verify(random, times(3)).nextInt();
}
}
4) 参数匹配
Mockito提供了多种参数匹配器(Matchers)用于更灵活的验证和配置行为:
java
import static org.mockito.ArgumentMatchers.*;
when(mockRepository.findById(anyInt())).thenReturn(Optional.of(user));
verify(mockRepository).findById(eq(1));
常见的匹配器包括:
any()
:匹配任何参数anyInt()
:匹配任何整数参数eq(value)
:匹配特定值isNull()
:匹配null值notNull()
:匹配非null值
5) 静态方法
添加依赖
xml
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
如果jdk版本低的话,版本可以低一点.
使用 mockStatic() 方法来 mock静态方法的所属类,此方法返回一个具有作用域的模拟对象。
java
@Test
public void testJoinWith() {
// 使用 Mockito 框架模拟 StringUtils 类的静态方法
MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);
// 创建一个字符串列表,作为 joinWith 方法的输入参数
List<String> stringList = Arrays.asList("a", "b", "c");
// 配置模拟行为,当调用 StringUtils.joinWith(",", stringList) 时,返回 "a,b,c"
stringUtilsMockedStatic.when(() -> StringUtils.joinWith(",", stringList)).thenReturn("a,b,c");
// 断言验证模拟行为是否正确,即 joinWith 方法返回的字符串是否与预期的 "a,b,c" 相等
Assertions.assertTrue(StringUtils.joinWith(",", stringList).equals("a,b,c"));
}
但是如果你写成下面这样子的话,会发送报错
java
package com.ucarinc.framework;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import java.util.Arrays;
import java.util.List;
class Demo2ApplicationTests {
@Test
public void testJoinWith() {
// 使用 Mockito 框架模拟 StringUtils 类的静态方法
MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);
// 创建一个字符串列表,作为 joinWith 方法的输入参数
List<String> stringList = Arrays.asList("a", "b", "c");
// 配置模拟行为,当调用 StringUtils.joinWith(",", stringList) 时,返回 "a,b,c"
stringUtilsMockedStatic.when(() -> StringUtils.joinWith(",", stringList)).thenReturn("a,b,c");
// 断言验证模拟行为是否正确,即 joinWith 方法返回的字符串是否与预期的 "a,b,c" 相等
Assertions.assertTrue(StringUtils.joinWith(",", stringList).equals("a,b,c"));
}
/**
* 测试StringUtils类中的join方法。
* 该测试使用Mockito框架来模拟静态方法的行为,验证join方法是否按照预期工作。
* */
@Test
public void testJoin() {
// 使用Mockito模拟StringUtils类的静态方法
MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);
// 创建一个字符串列表作为join方法的输入
List<String> stringList = Arrays.asList("a", "b", "c");
// 配置模拟行为,当调用StringUtils.join(",", stringList)时,返回字符串"a,b,c"
stringUtilsMockedStatic.when(() -> StringUtils.join(",", stringList)).thenReturn("a,b,c");
// 断言验证模拟行为是否正确,即 join 方法返回的字符串是否与预期的 "a,b,c" 相等
Assertions.assertTrue(StringUtils.join(",", stringList).equals("a,b,c"));
}
}
然后执行整个测试类后会报错:,就会报错
原因是因为 mockStatic() 方法是将当前需要 mock 的类注册到本地线程上(ThreadLocal),而这个注册在一次 mock 使用完之后是不会消失的,需要我们手动的去销毁。如过没有销毁,再次 mock 这个类的时候 Mockito 将会提示我们 :"当前对象 mock 的对象已经在线程中注册了,请先撤销注册后再试"。这样做的目的也是为了保证模拟出来的对象之间是相互隔离的,保证同时和连续的测试不会收到上下文的影响。
2.2 常用注解
1) @Mock
快速 mock
的方法,使用 @mock
注解。
mock 注解需要搭配 MockitoAnnotations.openMocks(testClass)
方法一起使用。
java
package com.ucarinc.framework;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import static org.mockito.Mockito.*;
public class App2Test {
@Mock
private Random random;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
/**
* 测试Mockito框架的使用,模拟Random类的nextInt方法。
* 该测试函数没有参数和返回值,主要用于演示Mockito的基本用法。
*/
@Test
public void test02() {
// 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)
System.out.println("第一次:"+random.nextInt());
// 指定当调用nextInt()时,始终返回1
Mockito.when(random.nextInt()).thenReturn(1);
System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1
// 断言nextInt()方法返回值是否为1
Assertions.assertEquals(1,random.nextInt());
// 验证nextInt()方法是否被调用了两次
verify(random, times(3)).nextInt();
}
}
2) @BeforeEach 与 @BeforeAfter
java
package com.ucarinc.framework;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
public class RandomTest02 {
private final Logger log= LoggerFactory.getLogger(RandomTest02.class);
@Mock
private Random random;
@BeforeEach
void setUp() {
log.info("==============测试前准备===============");
MockitoAnnotations.openMocks(this);
}
/**
* 测试Mockito框架的使用,模拟Random类的nextInt方法。
* 该测试函数没有参数和返回值,主要用于演示Mockito的基本用法。
*/
@Test
public void test02() {
// 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)
System.out.println("第一次:"+random.nextInt());
// 指定当调用nextInt()时,始终返回1
Mockito.when(random.nextInt()).thenReturn(1);
System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1
// 断言nextInt()方法返回值是否为1
Assertions.assertEquals(1,random.nextInt());
// 验证nextInt()方法是否被调用了两次
verify(random, times(3)).nextInt();
}
@AfterEach
void tearDown() {
log.info("==============测试后结果===============");
}
}
3) @InjectMocks
@InjectMocks
用于将模拟对象注入到被测试类中的相应字段。通过该注解可以自动将模拟对象注入到被测试类中标记为@InjectMocks的字段中,可以理解为使用@Mock创建出来的对象注入到@InjectMocks创建的对象中,这样被测试类就可以使用模拟对象作为其依赖了。
java
package com.ucarinc.framework;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class App6Test {
@Mock
AClass aClass;
@InjectMocks
BClass bClass;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testAdd() {
// 当调用a方法时,直接返回1000。a是模拟的
when(aClass.add()).thenReturn(1000);
Assertions.assertEquals(1003, bClass.add(1,2));
}
public static class AClass{
public AClass(){
}
public int add(){
System.out.println("AClass.add");
return 1;
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class BClass {
private AClass aClass;
public int add(int a, int b) {
// 调用a方法
int add = aClass.add();
System.out.println("测试了a+b a=" + a + ",b=" + b + ",add=" + add);
return a + b + add;
}
}
}
通常配合@Mock注解一起使用,一般用作service层。然后把mock的mapper层注入其中
java
@InjectMocks
private UserService userService;
@MockBean
private UserMapper userMapper;
4) @Spy
spy()
方法与 mock()
方法不同的是
- 被
spy
的对象会走真实的方法,而mock
对象不会spy()
方法的参数是对象实例,mock
的参数是 class
首先,我们使用mock方法。做一个测试
java
package com.ucarinc.framework;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import java.util.Random;
import static org.mockito.Mockito.*;
public class App3Test {
public static class MockitoTestController{
public int add(int a, int b){
System.out.println("测试了a+b a="+a+",b="+b);
return a+b;
}
}
@Test
public void test01() {
MockitoTestController mockitoTestController =new MockitoTestController();
// 调用实际的 mockitoTestController 对象的 add 方法,并验证结果是否为预期值
int result = mockitoTestController.add(1, 2);
Assertions.assertEquals(3, result);
// 使用 Mockito 创建 mockitoTest 的 mock 对象,并对它调用 add 方法,然后验证结果
MockitoTestController mockitoTest = Mockito.mock(MockitoTestController.class);
int result1 = mockitoTest.add(1, 2);
Assertions.assertEquals(3, result1);
}
}
返回的结果
第二个 Assertions 断言失败,因为没有给 mockitoTest 对象打桩,因此返回默认值
使用@Spy()注解示例。引入依赖
xml
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ucarinc.framework</groupId>
<artifactId>demo1</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo1</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.13</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
代码测试
java
package com.ucarinc.framework;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
public class App4Test {
private final Logger log= LoggerFactory.getLogger(App4Test.class);
public static class MockitoTestController{
public int add(int a, int b){
System.out.println("测试了a+b a="+a+",b="+b);
return a+b;
}
}
@Spy
private MockitoTestController mockitoTestController;
@BeforeEach
void setUp() {
}
/**
* 测试add方法
* 该方法模拟调用mockitoTestController的add方法,传入参数1和2,期望返回值为3。
* 首先,通过when语句设置mockitoTestController的add方法返回值为3;
* 然后,使用assertThat断言验证调用add方法(1, 2)实际返回值确实为3;
* 最后,通过verify语句确认mockitoTestController的add方法确实被调用了一次,并传入了参数1和2。
*/
@Test
void testAdd() {
// 设置mock对象的行为(打桩),当调用add(1, 2)时返回4
when(mockitoTestController.add(1, 2)).thenReturn(4);
// 调用mock对象的方法,返回为4
int result = mockitoTestController.add(1, 2);
log.info("mockitoTestController.add result={}",result);
// 断言验证:调用add(1, 2)方法返回值是否为4
Assertions.assertEquals(mockitoTestController.add(1, 2),4);
// 验证:确保add方法(1, 2)被调用了一次
verify(mockitoTestController,times(2)).add(1, 2);
}
}
5) @Captor
接下来,我们来看看如何使用@Captor注解来创建ArgumentCaptor实例。
在以下示例中,我们将在不使用@Captor注释的情况下创建ArgumentCaptor:
java
@Test
public void whenNotUseCaptorAnnotation_thenCorrect() {
List mockList = Mockito.mock(List.class);
ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);
mockList.add("one");
Mockito.verify(mockList).add(arg.capture());
assertEquals("one", arg.getValue());
}
使用@Captor来创建一个ArgumentCaptor实例:
java
@Mock
List<String> mockedList;
@Captor
ArgumentCaptor<String> argCaptor;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
public void whenUseCaptorAnnotation_thenTheSame() {
mockedList.add("one");
verify(mockedList).add(argCaptor.capture());
assertEquals("one", argCaptor.getValue());
}
6) @RunWith和@ExtendWith
测试类上使用 @RunWith(SpringRunner.class) 注解(使用的是 JUnit 4)
测试类上使用 @ExtendWith(SpringExtension.class)注解(使用的是 JUnit 5)
SpringBoot2.4.x之后,改为默认仅集成JUnit5,干掉了兼容JUnit4
@RunWith
- @RunWith就是一个运行器
- @RunWith(JUnit4.class)就是指用JUnit4来运行
- @RunWith(SpringJUnit4ClassRunner.class),让测试运行于
Spring
测试环境,以便在测试开始的时候自动创建Spring
的应用上下文
java
@RunWith(SpringRunner.class) //14.版本之前用的是SpringJUnit4ClassRunner.class
@SpringBootTest(classes = Application.class) //1.4版本之前用的是//@SpringApplicationConfiguration(classes = Application.class)
public class SystemInfoServiceImplTest {
@Autowired
private ISystemInfoService systemInfoservice;
@Test
public void add() throws Exception {
}
@Test
public void findAll() throws Exception {
}
}
@ExtendWith
@ExtendWith 具体Demo展示如下:
java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
// 定义一个自定义的JUnit扩展,用于在测试开始前输出日志
class CustomExtension implements BeforeTestExecutionCallback {
@Override
public void beforeTestExecution(ExtensionContext context) {
System.out.println("Before Test Execution");
}
}
// 使用@ExtendWith注解加载自定义扩展
@ExtendWith(CustomExtension.class)
public class test {
@Test
void test1() {
System.out.println("Test 1");
Assertions.assertTrue(true);
}
@Test
void test2() {
System.out.println("Test 2");
Assertions.assertEquals(2, 1 + 1);
}
}
Mockito通常与JUnit结合使用,特别是JUnit 5,利用@ExtendWith(MockitoExtension.class)
简化Mock对象的初始化
启动类加上@ExtendWith(MockitoExtension.class),会自动处理@Mock
,@Spy
,@InjectMocks
等注解
java
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
// 测试代码
}
2.3 常见区别
Mock对象和Spy对象区别
方法插桩 | 方法不插桩 | 作用对象 | 最佳实践 | |
---|---|---|---|---|
mock对象 | 执行插桩逻辑 | 返回mock对象的默认值 | 类、接口 | 被测试类或其依赖 |
spy对象 | 执行插桩逻辑 | 调用真实方法 | 类、接口 | 被测试类 |
3. Springboot 使用
首先看下完整的pom结构
3.1 数据准备
创建sql
sql
create database if not exists mockito;
use mockito;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`
(
id BIGINT NOT NULL COMMENT '主键ID',
name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名',
age INT NULL DEFAULT NULL COMMENT '年龄',
email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱',
PRIMARY KEY (id)
);
INSERT INTO `user` (id, name, age, email)
VALUES (1, 'Jone', 18, 'test1@baomidou.com'),
(2, 'Jack', 20, 'test2@baomidou.com'),
(3, 'Tom', 28, 'test3@baomidou.com'),
(4, 'Sandy', 21, 'test4@baomidou.com'),
(5, 'Billie', 24, 'test5@baomidou.com');
引入依赖
创建springboot 项目。
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">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.lkcoffee.framework</groupId>
<artifactId>demo2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo2</name>
<description>demo2</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.3.1</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- springbbot配置-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
<version>8.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.28</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.lkcoffee.framework.demo2.Demo2Application</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
添加application.yml
java
server:
port: 8080
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1024MB
application:
name: demo2
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/mockito?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
mybatis-plus:
global-config:
db-config:
logic-delete-field: isDelete
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:mapper/**/*Mapper.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
file:
name: test.log
level:
root: INFO
org:
springframework: DEBUG
example:
springboottest: DEBUG
在Springboot 启动类中添加 @MapperScan
注解,扫描 Mapper 文件夹:
java
package com.lkcoffee.framework.demo2;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.lkcoffee.framework.demo2.mapper")
@SpringBootApplication
public class Demo2Application {
public static void main(String[] args) {
SpringApplication.run(Demo2Application.class, args);
}
}
编写实体类
java
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
编写 Mapper 接口类 UserMapper.java
:
java
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
}
编写Service层
java
package com.lkcoffee.framework.demo2.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.lkcoffee.framework.demo2.domain.User;
import java.util.List;
/**
* @Desciption: 用户服务层
* @Author: feixiang.li
* @date: 2024-07-11 19:51
**/
public interface UserService extends IService<User> {
/**
* 查询所有用户信息
* @return 所有用户信息
*/
List<User> queryAll();
/**
* 根据用户id查询
* @param id 用户id
* @return 用户信息
*/
User queryById(Long id);
/**
* 添加用户id
* @param user 用户信息
* @return 操作结果
*/
Boolean addUser(User user);
/**
* 根据用户id修改用户信息
* @param user
* @return
*/
Integer updateUser(User user);
}
实现Service层
java
package com.lkcoffee.framework.demo2.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.mapper.UserMapper;
import com.lkcoffee.framework.demo2.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
/**
* @Desciption: 用户操作类
* @Author: feixiang.li
* @date: 2024-07-12 10:39
**/
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public List<User> queryAll() {
log.info("被真实调用了, 执行了 查询所有用户信息");
return list();
}
@Override
public User queryById(Long id) {
log.info("被真实调用了, 根据用户id:{} 查询用户",id);
return getById(id);
}
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean addUser(User user) {
log.info("被真实调用了, 添加用户信息:{}",user);
if(Objects.nonNull(user.getId())){
throw new RuntimeException("被真实调用了,新增用户,id应该为空");
}
if(Objects.isNull(user.getAge()) || user.getAge() < 0 || user.getAge() > 100){
throw new RuntimeException("被真实调用了,请填写正确的年龄");
}
if(StringUtils.isBlank(user.getName())){
throw new RuntimeException("被真实调用了,对不起,姓名不能为空");
}
return save(user);
}
@Transactional(rollbackFor = Exception.class)
@Override
public Integer updateUser(User user) {
System.out.println("执行了真实的更新用户方法");
int result= getBaseMapper().updateById(user);
System.out.println("update user result:"+result);
return result;
}
}
编写controller
java
package com.lkcoffee.framework.demo2.controller;
import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* @Desciption:
* @Author: feixiang.li
* @date: 2024-07-12 10:45
**/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping
public List<User> queryAll(){
return userService.queryAll();
}
@GetMapping("/{id}")
public User queryById(@PathVariable Long id){
if(Objects.isNull(id)){
return new User();
}
return userService.queryById(id);
}
@PostMapping
public String save(@RequestBody User user){
if(Objects.isNull(user)){
return "对象为空";
}
userService.save(user);
return "success";
}
}
启动项目: 访问下面
bash
http://localhost:8080/user
返回一下结果,说明项目启动成功;
3.2 测试
1) 创建Mock或者Spy对象
junit4 | junit5 | |
---|---|---|
方法一 | @RunWith(MockitojUnitRunner.class)+@Mock等注解 | @ExtendWith(MockitoExtension.class)+@Mock等注解 |
方法二 | Mockito.mock(X.class)MockitoAnnotations.open等静态方法 | Mockito.mock(X.class)MockitoAnnotations.open等静态方法 |
方法三 | Mocks(this)+@Mock等注解 | Mocks(this)+@Mock等注解 |
方法一
java
package com.lkcoffee.framework.demo2;
/**
* @Desciption:
* @Author: feixiang.li
* @date: 2024-07-12 14:38
**/
import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension .class)
public class Test1 {
@Mock
private UserService mockUserService;
@Spy
private UserService spyUserService;
@Test
public void test1(){
// 判断某个对象是不是mock对象
System.out.println("Mockito.mockingDetails(mockUserService).isMock(): "+ Mockito.mockingDetails(mockUserService).isMock());
System.out.println("Mockito.mockingDetails(spyUserService).isSpy(): "+ Mockito.mockingDetails(spyUserService).isSpy());
}
}
方法二
java
package com.lkcoffee.framework.demo2;
/**
* @Desciption:
* @Author: feixiang.li
* @date: 2024-07-12 14:38
**/
import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
public class Test2 {
@Mock
private UserService mockUserService;
@Spy
private UserService spyUserService;
@BeforeEach
public void init() {
mockUserService=Mockito.mock(UserService.class);
spyUserService=Mockito.spy(UserService.class);
}
@Test
public void test1(){
// 判断某个对象是不是mock对象
System.out.println("Mockito.mockingDetails(mockUserService).isMock(): "+ Mockito.mockingDetails(mockUserService).isMock());
System.out.println("Mockito.mockingDetails(spyUserService).isSpy(): "+ Mockito.mockingDetails(spyUserService).isSpy());
}
}
方法三
java
package com.lkcoffee.framework.demo2;
/**
* @Desciption:
* @Author: feixiang.li
* @date: 2024-07-12 14:38
**/
import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
public class Test3 {
@Mock
private UserService mockUserService;
@Spy
private UserService spyUserService;
@BeforeEach
public void init() {
MockitoAnnotations.openMocks(this);
}
@Test
public void test1(){
// 判断某个对象是不是mock对象
System.out.println("Mockito.mockingDetails(mockUserService).isMock(): "+ Mockito.mockingDetails(mockUserService).isMock());
System.out.println("Mockito.mockingDetails(spyUserService).isSpy(): "+ Mockito.mockingDetails(spyUserService).isSpy());
}
}
MockitoAnnotations.initMocks(this)和MockitoAnnotations.openMocks(this)
这两个效果一样,只是在juit5中initMocks被抛弃了
MockitoAnnotations.initMocks(this)方法并不会产生代理类,它主要是用于初始化Mockito注解。在测试中,我们通常使用@Mock、@Spy、@InjectMocks等注解来创建Mock对象,并使用Mockito.when、Mockito.verify等方法来模拟对象的行为和验证方法调用。
但是,如果我们不调用MockitoAnnotations.initMocks(this)方法,这些Mock对象就无法被正确初始化,从而导致测试失败。因此,我们通常在@Before注解方法中调用这个方法,以确保所有的Mock对象都已经被正确初始化。
在具体实现中,MockitoAnnotations.initMocks(this)方法会扫描测试类中所有的@Mock、@Spy、@InjectMocks注解,并根据注解中的类型和名称来创建对应的Mock对象,并将这些对象注入到测试类中。这样,在测试过程中就可以使用这些Mock对象来模拟外部依赖,从而实现单元测试的独立性和可重复性。
2) 参数匹配
java
package com.lkcoffee.framework.demo2;
/**
* @Desciption:
* @Author: feixiang.li
* @date: 2024-07-12 14:38
**/
import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
/**
* 参数匹配:通过方法签名(参数)来制定哪些方法调用需要被处理
*/
@ExtendWith(MockitoExtension.class)
public class ParamMatcherTest {
private final Logger log = LoggerFactory.getLogger(ParamMatcherTest.class);
@Mock
private UserService mockUserService;
@Spy
private UserService spyUserService;
@Test
public void test2() {
/**
* 这里返回值是null. Mock对象不会调用真实方法
*/
User user = new User();
user.setId(1L);
user.setName("fly");
doReturn(99).when(mockUserService).updateUser(user);
int result1 = mockUserService.updateUser(user);
log.info("用户1修改对象返回值:{}", result1);
User user2 = new User();
user.setId(2L);
user.setName("name2");
int result2 = mockUserService.updateUser(user2);
log.info("用户2修改对象返回值:{}", result2);
// 现在我想任意用户都返回99
doReturn(99).when(mockUserService).updateUser(any());
result1 = mockUserService.updateUser(user);
result2 = mockUserService.updateUser(user2);
log.info("用户1修改对象返回值:{}", result1);
log.info("用户2修改对象返回值:{}", result2);
}
@Test
public void test1() {
/**
* 这里返回值是null. Mock对象不会调用真实方法。如果不进行插桩的话
*/
User user = mockUserService.queryById(1L);
log.info("user:{}", user);
}
}
3) 打桩
java
package com.lkcoffee.framework.demo2;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
public class App4Test {
private final Logger log= LoggerFactory.getLogger(App4Test.class);
public static class MockitoTestController{
public int add(int a, int b){
System.out.println("调用了真实方法 测试了a+b a="+a+",b="+b);
return a+b;
}
}
@Spy
private MockitoTestController spyMockitoTestController;
@BeforeEach
void setUp() {
}
/**
* 测试add方法
* 该方法模拟调用mockitoTestController的add方法,传入参数1和2,期望返回值为3。
* 首先,通过when语句设置mockitoTestController的add方法返回值为3;
* 然后,使用assertThat断言验证调用add方法(1, 2)实际返回值确实为3;
* 最后,通过verify语句确认mockitoTestController的add方法确实被调用了一次,并传入了参数1和2。
*/
@Test
void testAdd() {
// 设置mock对象的行为(打桩),当调用add(1, 2)时返回4
// 虽然使用了when ,但是已经调用了真实方法
when(spyMockitoTestController.add(1, 2)).thenReturn(4);
// 调用mock对象的方法,返回为4
int result = spyMockitoTestController.add(1, 2);
log.info("mockitoTestController.add result={}",result);
// 断言验证:调用add(1, 2)方法返回值是否为4
Assertions.assertEquals(spyMockitoTestController.add(1, 2),4);
// 验证:确保add方法(1, 2)被调用了一次
verify(spyMockitoTestController,times(2)).add(1, 2);
/**
* spy对象在没有摄性时是谓用真实方法的,号加en中会导致先技行一次方法,达不打桩的目的
* 需使用 doXxx().when(obj).someNethod()
*/
doReturn(99).when(spyMockitoTestController).add(anyInt(),anyInt());
int result2 = spyMockitoTestController.add(1, 2);
log.info("spyMockitoTestController.add result={}",result2);
}
}
如果使用springboot的话,低端用法,没有使用@SpringbootTest
和@SpyBean
注解
java
package com.lkcoffee.framework.demo2;
/**
* @Desciption:
* @Author: feixiang.li
* @date: 2024-07-12 14:38
**/
import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.mapper.UserMapper;
import com.lkcoffee.framework.demo2.service.UserService;
import com.lkcoffee.framework.demo2.service.impl.UserServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class Test5 {
@Mock
private UserMapper userMapper;
@Mock
private UserServiceImpl mockUserService;
@InjectMocks
@Spy
private UserServiceImpl spyUserService;
@Test
public void test1() {
// 这一步是为了解决mybatisplus的问题。手动把mapper注入进去。
// 如果使用了Autowired 的 Resource ,就不需要这一步了
doReturn(userMapper).when(spyUserService).getBaseMapper();
User user = new User();
user.setId(1L);
user.setName("name1");
when(userMapper.updateById(any(User.class))).thenReturn(-1);
when(mockUserService.updateUser(user)).thenReturn(99);
int result1 = mockUserService.updateUser(user);
System.out.println("result1 = " + result1);
when(spyUserService.updateUser(user)).thenReturn(99);
int result2 = spyUserService.updateUser(user);
System.out.println("result2 = " + result2);
/**
* spy对象在没有摄性时是谓用真实方法的,号加en中会导致先技行一次方法,达不och的目的
* 需使用 doXxx().when(obj).someNethod()
*/
doReturn(100).when(spyUserService).updateUser(any());
int result3 = spyUserService.updateUser(user);
System.out.println("result3 = " + result3);
}
}
执行结果对象
xml
result1 = 99
执行了真实的更新用户方法
update user result:-1
result2 = 99
result3 = 100
4) 多次打桩
java
package com.lkcoffee.framework.demo2;
/**
* @Desciption:
* @Author: feixiang.li
* @date: 2024-07-12 14:38
**/
import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.mapper.UserMapper;
import com.lkcoffee.framework.demo2.service.impl.UserServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class Test6 {
@Mock
private List<Integer> mockList;
@Test
public void test1() {
//第1次调用返回1,第2次调用返回2,第3次及之后的调用都返回3
// when(mockList.size()).thenReturn(1).thenReturn(2).thenReturn(3),
// 可简写为:
when(mockList.size()).thenReturn(1, 2, 3);
Assertions.assertEquals(1, mockList.size());
Assertions.assertEquals(2, mockList.size());
Assertions.assertEquals(3, mockList.size());
Assertions.assertEquals(3, mockList.size());
}
}
5) 实战
java
package com.lkcoffee.framework.demo2;
import com.lkcoffee.framework.demo2.domain.User;
import com.lkcoffee.framework.demo2.mapper.UserMapper;
import com.lkcoffee.framework.demo2.service.UserService;
import com.lkcoffee.framework.demo2.service.impl.UserServiceImpl;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@SpringBootTest(classes = Demo2Application.class)
class UserServiceImplTest {
@MockBean
private UserMapper userMapper;
@Resource
@SpyBean
private UserServiceImpl userService;
@BeforeEach
void setUp() {
// 这一步是为了解决mybatisplus 中没有baseMapper的问题
// 因为是继承了ServiceImpl 。是父类。InjectMocks无法注入父类的属性
// 如果使用了Autowired 的 Resource ,就不需要这一步了
// doReturn(userMapper).when(userService).getBaseMapper();
}
@Test
void testQueryAll() {
// 模拟查询结果
when(userMapper.selectList(any())).thenReturn(List.of(
new User(1L, "Alice", 25,"203462009@qq.com"),
new User(2L, "Bob", 30,"203462008@qq.com")
));
// 执行查询
var result = userService.queryAll();
// 验证查询结果
assertEquals(2, result.size());
assertEquals("Alice", result.get(0).getName());
assertEquals("Bob", result.get(1).getName());
}
@Test
void testQueryById() {
// 模拟查询结果
when(userMapper.selectById(1L)).thenReturn(new User(1L, "Alice", 25,"203462009@qq.com"));
// 执行查询
var result = userService.queryById(1L);
// 验证查询结果
assertEquals("Alice", result.getName());
}
@Test
void testAddUser() {
// 创建一个用户对象
User user = new User(null, "Alice", 25,"203462009@qq.com");
// 模拟save方法返回结果
when(userMapper.insert(user)).thenReturn(1);
// 执行添加用户
var result = userService.addUser(user);
// 验证添加结果
assertTrue(result);
}
}
3.3 Springboot测试注解
@MockBean
@MckBean
是Spring Boot提供的注解,专门用于在Spring应用上下文中添加或替换一个bean为mock对象。这个注解主要用于集成测试场景,特别是当测试需要Spring环境支持时,如测试控制器与服务层的交互等。
- 使用@MockBean注解的的对象,会生成一个Mock的bean.不会生成原来的bean
- 并会将该bean注入到依赖该bean的其他bean中
- 正常的bean还是会正常组装注入
Spring Boot 中@Mock 和@MockBean 注解的主要区别
@Mock
用于模拟不属于 Spring 上下文的对象,而@MockBean
用于模拟属于一部分的对象Spring上下文的。它用于带有 Mockito 框架的普通 JUnit 测试。它也不知道 Spring 上下文,通常用于单元测试隔离组件,而不需要完整的 Spring 上下文设置。@MockBean
是一个 Spring Boot 特定的注释,它提供与 Spring Boot 测试模块的集成,允许在 Spring Boot 应用程序中无缝模拟 Spring bean。@Mock
需要使用 MockitoJUnitRunner 或 MockitoExtension 来初始化模拟对象,而@MockBean在测试上下文设置期间由 Spring Boot 测试框架自动初始化。@MockBean
在测试时将Spring上下文中的实际bean替换为mock对象,而@Mock
不影响Spring上下文中的实际bean
@SpyBean
java
@SpringBootTest(classes = AppBootStrap.class)
public class AbstractTestCase {}
/**
* 1。使用@MockBean注解的的对象,会生成一个Mock的bean.不会生成原来的bean
* 2。并会将该bean注入到依赖该bean的其他bean中
* 3。正常的bean还是会正常组装注入
*/
public class HelloControllerMockBeanTest extends AbstractTestCase {
@Autowired
private HelloController helloController;
@MockBean
private HelloService helloService;
@Test
public void testHello(){
System.out.println("============only junit5================");
helloController.hello();
System.out.println("============only junit5================");
}
}
/**
* 1。使用@MockBean注解的的对象,会生成一个spy的bean行为与原类型一致.不会生成原来的bean
* 2。并会将该bean注入到依赖该bean的其他bean中
* 3。正常的bean还是会正常组装注入
*/
public class HelloControllerSpyBeanTest extends AbstractTestCase {
@Autowired
private HelloController helloController;
@SpyBean
private HelloService helloService;
@Test
public void testHello(){
System.out.println("============only junit5================");
helloController.hello();
System.out.println("============only junit5================");
}
}