赞
踩
名称 | 链接 | 备注 |
---|---|---|
mockito英文文档 | Mockito (Mockito 5.12.0 API) (javadoc.io) | |
mockito中文文档 | Mockito 中文文档 ( 2.0.26 beta ) - 《Mockito 框架中文文档》 - 极客文档 (geekdaxue.co) | |
视频教学链接 | https://www.bilibili.com/video/BV1P14y1k7Hi |
Mockito是Java生态系统中最受欢迎的单元测试模拟框架之一,以其简洁易用的API和强大的模拟能力赢得了广大开发者的青睐。Mockito允许我们在不实际依赖外部资源的情况下对代码进行彻底且高效的单元测试,极大地提升了测试覆盖率和代码质量。
Mockito是一种模拟框架,其核心概念是在测试过程中创建并使用“Mock对象”。Mock对象是对实际对象的一种模拟,它继承或实现了被测试类所依赖的接口或类,但其行为可以根据测试需求自由定制。控制其在测试环境下的行为,从而将注意力聚焦于类本身的逻辑验证上。
Mockito
的底层原理是使用 cglib
动态生成一个 代理类对象,因此,mock
出来的对象其实质就是一个 代理,该代理在 没有配置/指定行为 的情况下,默认返回空值
创建一个普通的maven项目。添加依赖
<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>
使用Mockito.mock()
方法创建接口或抽象类的Mock对象。下面是它的方法接口
public static <T> T mock(Class<T> classToMock)
- classToMock:待 mock 对象的 class 类。
- 返回 mock 出来的类
实例:使用 mock 方法 mock 一个类
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等注解 |
使用when
和thenReturn
方法配置Mock对象的行为:
打桩可以理解为mock
对象规定一行的行为,使其按照我们的要求来执行具体的操作。在Mockito
中,常用的打桩方法为
方法 | 含义 |
---|---|
when().thenReturn() | Mock 对象在触发指定行为后返回指定值 |
when().thenThrow() | Mock 对象在触发指定行为后抛出指定异常 |
when().doCallRealMethod() | Mock 对象在触发指定行为后调用真实的方法 |
thenReturn() 代码示例
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
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); } }
你还可以配置方法抛出异常:
/** * 测试当调用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返回值打桩
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(); } }
Mock对象进行行为验证和结果断言。验证是校验对象是否发生过某些行为,Mockito 中验证的方法是:verify
。
常见的验证方法包括:
verify(mock).methodCall()
:验证方法被调用verify(mock, times(n)).methodCall()
:验证方法被调用n次verify(mock, never()).methodCall()
:验证方法从未被调用验证交换:Verify 配合 time() 方法,可以校验某些操作发生的次数。
注意:当使用 mock 对象时,如果不对其行为进行定义,则 mock 对象方法的返回值为返回类型的默认值。
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(); } }
Mockito提供了多种参数匹配器(Matchers)用于更灵活的验证和配置行为:
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值添加依赖
<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静态方法的所属类,此方法返回一个具有作用域的模拟对象。
@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")); }
但是如果你写成下面这样子的话,会发送报错
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 的对象已经在线程中注册了,请先撤销注册后再试“。这样做的目的也是为了保证模拟出来的对象之间是相互隔离的,保证同时和连续的测试不会收到上下文的影响。
快速 mock
的方法,使用 @mock
注解。
mock 注解需要搭配 MockitoAnnotations.openMocks(testClass)
方法一起使用。
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(); } }
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("==============测试后结果==============="); } }
@InjectMocks
用于将模拟对象注入到被测试类中的相应字段。通过该注解可以自动将模拟对象注入到被测试类中标记为@InjectMocks的字段中,可以理解为使用@Mock创建出来的对象注入到@InjectMocks创建的对象中,这样被测试类就可以使用模拟对象作为其依赖了。
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层注入其中
@InjectMocks
private UserService userService;
@MockBean
private UserMapper userMapper;
spy()
方法与 mock()
方法不同的是
- 被
spy
的对象会走真实的方法,而mock
对象不会spy()
方法的参数是对象实例,mock
的参数是 class
首先,我们使用mock方法。做一个测试
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()注解示例。引入依赖
<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>
代码测试
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); } }
接下来,我们来看看如何使用@Captor注解来创建ArgumentCaptor实例。
在以下示例中,我们将在不使用@Captor注释的情况下创建ArgumentCaptor:
@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实例:
@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()); }
测试类上使用 @RunWith(SpringRunner.class) 注解(使用的是 JUnit 4)
测试类上使用 @ExtendWith(SpringExtension.class)注解(使用的是 JUnit 5)SpringBoot2.4.x之后,改为默认仅集成JUnit5,干掉了兼容JUnit4
Spring
测试环境,以便在测试开始的时候自动创建Spring
的应用上下文@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 具体Demo展示如下:
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
等注解
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
// 测试代码
}
方法插桩 | 方法不插桩 | 作用对象 | 最佳实践 | |
---|---|---|---|---|
mock对象 | 执行插桩逻辑 | 返回mock对象的默认值 | 类、接口 | 被测试类或其依赖 |
spy对象 | 执行插桩逻辑 | 调用真实方法 | 类、接口 | 被测试类 |
首先看下完整的pom结构
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 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>
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 文件夹:
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); } }
import lombok.Data;
@Data
public class User {
private Long id;
private String name;
private Integer age;
private String email;
}
编写 Mapper 接口类 UserMapper.java
:
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
}
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层
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; } }
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"; } }
启动项目: 访问下面
http://localhost:8080/user
返回一下结果,说明项目启动成功;
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等注解 |
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()); } }
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()); } }
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对象来模拟外部依赖,从而实现单元测试的独立性和可重复性。
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); } }
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
注解
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); } }
执行结果对象
result1 = 99
执行了真实的更新用户方法
update user result:-1
result2 = 99
result3 = 100
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()); } }
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); } }
@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@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================"); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。