当前位置: 首页 > news >正文

单元测试Mockito笔记

文章目录

  • 单元测试Mockito
  • 1. 入门
    • 1.1 什么是Mockito
    • 1.2 优势
    • 1.3 原理
  • 2. 使用
    • 2.0 环境准备
    • 2.1 Mock
      • 1) Mock对象创建
      • 2) 配置Mock对象的行为(打桩)
      • 3) 验证方法调用
      • 4) 参数匹配
      • 5) 静态方法
    • 2.2 常用注解
      • 1) @Mock
      • 2) @BeforeEach 与 @BeforeAfter
      • 3) @InjectMocks
      • 4) @Spy
      • 5) @Captor
      • 6) @RunWith和@ExtendWith
        • @RunWith
        • @ExtendWith
    • 2.3 常见区别
      • Mock对象和Spy对象区别
  • 3. Springboot 使用
    • 3.1 数据准备
      • 创建sql
      • 引入依赖
      • 添加application.yml
      • 编写实体类
      • 编写Service层
      • 编写controller
    • 3.2 测试
      • 1) 创建Mock或者Spy对象
        • 方法一
        • 方法二
        • 方法三
      • 2) 参数匹配
      • 3) 打桩
      • 4) 多次打桩
      • 5) 实战
    • 3.3 Springboot测试注解
      • @MockBean
      • @SpyBean

单元测试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项目。添加依赖

<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对象。下面是它的方法接口

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 {@Testpublic 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");}
}  

总结

junit4junit5
方法一@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对象的行为(打桩)

使用whenthenReturn方法配置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;}}@Testvoid testAdd() {MockitoTestController mockitoTestController = mock(MockitoTestController.class);// 设置mock对象的行为(打桩),当调用add(1, 2)时返回4when(mockitoTestController.add(1, 2)).thenReturn(4);// 调用mock对象的方法,返回为4int result = mockitoTestController.add(1, 2);log.info("mockitoTestController.add result={}",result);// 断言验证:调用add(1, 2)方法返回值是否为4Assertions.assertEquals(mockitoTestController.add(1, 2),4);// 验证:确保add方法(1, 2)被调用了一次verify(mockitoTestController,times(2)).add(1, 2);}}

你还可以配置方法抛出异常:

 /*** 测试当调用add方法时抛出RuntimeException异常的情况。* 该测试函数不接受参数,也没有返回值。*/@Testvoid 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 {@MockList<String> mockList;@Testpublic 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 对象方法的返回值为返回类型的默认值。

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{@Testpublic 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()时,始终返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1// 断言nextInt()方法返回值是否为1Assertions.assertEquals(1,random.nextInt());// 验证nextInt()方法是否被调用了两次verify(random, times(3)).nextInt();}
}

4) 参数匹配

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值

5) 静态方法

添加依赖

    <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静态方法的所属类,此方法返回一个具有作用域的模拟对象。

    @Testpublic 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 {@Testpublic 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方法是否按照预期工作。* */@Testpublic 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"));}}

然后执行整个测试类后会报错:,就会报错

image-20240712094211482

原因是因为 mockStatic() 方法是将当前需要 mock 的类注册到本地线程上(ThreadLocal),而这个注册在一次 mock 使用完之后是不会消失的,需要我们手动的去销毁。如过没有销毁,再次 mock 这个类的时候 Mockito 将会提示我们 :”当前对象 mock 的对象已经在线程中注册了,请先撤销注册后再试“。这样做的目的也是为了保证模拟出来的对象之间是相互隔离的,保证同时和连续的测试不会收到上下文的影响。

2.2 常用注解

1) @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 {@Mockprivate Random random;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}/*** 测试Mockito框架的使用,模拟Random类的nextInt方法。* 该测试函数没有参数和返回值,主要用于演示Mockito的基本用法。*/@Testpublic void test02() {// 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)System.out.println("第一次:"+random.nextInt());// 指定当调用nextInt()时,始终返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1// 断言nextInt()方法返回值是否为1Assertions.assertEquals(1,random.nextInt());// 验证nextInt()方法是否被调用了两次verify(random, times(3)).nextInt();}}

2) @BeforeEach 与 @BeforeAfter

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);@Mockprivate Random random;@BeforeEachvoid setUp() {log.info("==============测试前准备===============");MockitoAnnotations.openMocks(this);}/*** 测试Mockito框架的使用,模拟Random类的nextInt方法。* 该测试函数没有参数和返回值,主要用于演示Mockito的基本用法。*/@Testpublic void test02() {// 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)System.out.println("第一次:"+random.nextInt());// 指定当调用nextInt()时,始终返回1Mockito.when(random.nextInt()).thenReturn(1);System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1// 断言nextInt()方法返回值是否为1Assertions.assertEquals(1,random.nextInt());// 验证nextInt()方法是否被调用了两次verify(random, times(3)).nextInt();}@AfterEachvoid tearDown() {log.info("==============测试后结果===============");}}

image-20240712095557244

3) @InjectMocks

@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 {@MockAClass aClass;@InjectMocksBClass bClass;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}@Testvoid 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@NoArgsConstructorpublic 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;

4) @Spy

spy() 方法与 mock() 方法不同的是

  1. spy 的对象会走真实的方法,而 mock 对象不会
  2. 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;}}@Testpublic 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 对象打桩,因此返回默认值

image-20240712100357578

使用@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;}}@Spyprivate MockitoTestController mockitoTestController;@BeforeEachvoid setUp() {}/*** 测试add方法* 该方法模拟调用mockitoTestController的add方法,传入参数1和2,期望返回值为3。* 首先,通过when语句设置mockitoTestController的add方法返回值为3;* 然后,使用assertThat断言验证调用add方法(1, 2)实际返回值确实为3;* 最后,通过verify语句确认mockitoTestController的add方法确实被调用了一次,并传入了参数1和2。*/@Testvoid testAdd() {// 设置mock对象的行为(打桩),当调用add(1, 2)时返回4when(mockitoTestController.add(1, 2)).thenReturn(4);// 调用mock对象的方法,返回为4int result = mockitoTestController.add(1, 2);log.info("mockitoTestController.add result={}",result);// 断言验证:调用add(1, 2)方法返回值是否为4Assertions.assertEquals(mockitoTestController.add(1, 2),4);// 验证:确保add方法(1, 2)被调用了一次verify(mockitoTestController,times(2)).add(1, 2);}}

5) @Captor

接下来,我们来看看如何使用@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实例:

    @MockList<String> mockedList;@CaptorArgumentCaptor<String> argCaptor;@BeforeEachvoid setUp() {MockitoAnnotations.openMocks(this);}@Testpublic 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的应用上下文
@RunWith(SpringRunner.class) //14.版本之前用的是SpringJUnit4ClassRunner.class
@SpringBootTest(classes = Application.class) //1.4版本之前用的是//@SpringApplicationConfiguration(classes = Application.class)
public class SystemInfoServiceImplTest {@Autowiredprivate ISystemInfoService systemInfoservice;@Testpublic void add() throws Exception {}@Testpublic void findAll() throws Exception {}}
@ExtendWith

@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 {@Overridepublic void beforeTestExecution(ExtensionContext context) {System.out.println("Before Test Execution");}
}// 使用@ExtendWith注解加载自定义扩展
@ExtendWith(CustomExtension.class)
public class test {@Testvoid test1() {System.out.println("Test 1");Assertions.assertTrue(true);}@Testvoid 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 {// 测试代码
}

2.3 常见区别

Mock对象和Spy对象区别

方法插桩方法不插桩作用对象最佳实践
mock对象执行插桩逻辑返回mock对象的默认值类、接口被测试类或其依赖
spy对象执行插桩逻辑调用真实方法类、接口被测试类

3. Springboot 使用

首先看下完整的pom结构

image-20240712113509616

3.1 数据准备

创建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 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

server:port: 8080spring:jackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8servlet:multipart:max-file-size: 1024MBmax-request-size: 1024MBapplication:name: demo2datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.1:3306/mockito?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: rootmybatis-plus:global-config:db-config:logic-delete-field: isDeletelogic-delete-value: 1logic-not-delete-value: 0mapper-locations: classpath*:mapper/**/*Mapper.xmlconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpllogging:file:name: test.loglevel:root: INFOorg:springframework: DEBUGexample: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> {}

编写Service层

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 {@Overridepublic List<User> queryAll() {log.info("被真实调用了, 执行了 查询所有用户信息");return list();}@Overridepublic User queryById(Long id) {log.info("被真实调用了, 根据用户id:{} 查询用户",id);return getById(id);}@Transactional(rollbackFor = Exception.class)@Overridepublic 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)@Overridepublic Integer updateUser(User user) {System.out.println("执行了真实的更新用户方法");int result= getBaseMapper().updateById(user);System.out.println("update user result:"+result);return result;}}

编写controller

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 {@Autowiredprivate UserService userService;@GetMappingpublic 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);}@PostMappingpublic String save(@RequestBody User user){if(Objects.isNull(user)){return "对象为空";}userService.save(user);return "success";}
}

启动项目: 访问下面

http://localhost:8080/user

返回一下结果,说明项目启动成功;

image-20240712105244873

3.2 测试

1) 创建Mock或者Spy对象

junit4junit5
方法一@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 {@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@Testpublic 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 {@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@BeforeEachpublic void init() {mockUserService=Mockito.mock(UserService.class);spyUserService=Mockito.spy(UserService.class);}@Testpublic 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 {@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@BeforeEachpublic void init() {MockitoAnnotations.openMocks(this);}@Testpublic 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) 参数匹配

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);@Mockprivate UserService mockUserService;@Spyprivate UserService spyUserService;@Testpublic 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);// 现在我想任意用户都返回99doReturn(99).when(mockUserService).updateUser(any());result1 = mockUserService.updateUser(user);result2 = mockUserService.updateUser(user2);log.info("用户1修改对象返回值:{}", result1);log.info("用户2修改对象返回值:{}", result2);}@Testpublic void test1() {/*** 这里返回值是null. Mock对象不会调用真实方法。如果不进行插桩的话*/User user = mockUserService.queryById(1L);log.info("user:{}", user);}
}

3) 打桩

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;}}@Spyprivate MockitoTestController spyMockitoTestController;@BeforeEachvoid setUp() {}/*** 测试add方法* 该方法模拟调用mockitoTestController的add方法,传入参数1和2,期望返回值为3。* 首先,通过when语句设置mockitoTestController的add方法返回值为3;* 然后,使用assertThat断言验证调用add方法(1, 2)实际返回值确实为3;* 最后,通过verify语句确认mockitoTestController的add方法确实被调用了一次,并传入了参数1和2。*/@Testvoid testAdd() {// 设置mock对象的行为(打桩),当调用add(1, 2)时返回4// 虽然使用了when ,但是已经调用了真实方法when(spyMockitoTestController.add(1, 2)).thenReturn(4);// 调用mock对象的方法,返回为4int result = spyMockitoTestController.add(1, 2);log.info("mockitoTestController.add result={}",result);// 断言验证:调用add(1, 2)方法返回值是否为4Assertions.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 {@Mockprivate UserMapper userMapper;@Mockprivate UserServiceImpl mockUserService;@InjectMocks@Spyprivate UserServiceImpl spyUserService;@Testpublic 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

4) 多次打桩

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 {@Mockprivate List<Integer> mockList;@Testpublic 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) 实战

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   {@MockBeanprivate UserMapper userMapper;@Resource@SpyBeanprivate UserServiceImpl userService;@BeforeEachvoid setUp() {// 这一步是为了解决mybatisplus 中没有baseMapper的问题// 因为是继承了ServiceImpl 。是父类。InjectMocks无法注入父类的属性// 如果使用了Autowired 的 Resource ,就不需要这一步了// doReturn(userMapper).when(userService).getBaseMapper();}@Testvoid 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());}@Testvoid testQueryById() {// 模拟查询结果when(userMapper.selectById(1L)).thenReturn(new User(1L, "Alice", 25,"203462009@qq.com"));// 执行查询var result = userService.queryById(1L);// 验证查询结果assertEquals("Alice", result.getName());}@Testvoid 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);}
}

image-20240712164239982

3.3 Springboot测试注解

@MockBean

@MckBean是Spring Boot提供的注解,专门用于在Spring应用上下文中添加或替换一个bean为mock对象。这个注解主要用于集成测试场景,特别是当测试需要Spring环境支持时,如测试控制器与服务层的交互等。

  1. 使用@MockBean注解的的对象,会生成一个Mock的bean.不会生成原来的bean
  2. 并会将该bean注入到依赖该bean的其他bean中
  3. 正常的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

@SpringBootTest(classes = AppBootStrap.class)
public class AbstractTestCase {}/*** 1。使用@MockBean注解的的对象,会生成一个Mock的bean.不会生成原来的bean* 2。并会将该bean注入到依赖该bean的其他bean中* 3。正常的bean还是会正常组装注入*/
public class HelloControllerMockBeanTest extends AbstractTestCase {@Autowiredprivate HelloController helloController;@MockBeanprivate HelloService helloService;@Testpublic 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 {@Autowiredprivate HelloController helloController;@SpyBeanprivate HelloService helloService;@Testpublic void testHello(){System.out.println("============only junit5================");helloController.hello();System.out.println("============only junit5================");}
}

相关文章:

单元测试Mockito笔记

文章目录 单元测试Mockito1. 入门1.1 什么是Mockito1.2 优势1.3 原理 2. 使用2.0 环境准备2.1 Mock1) Mock对象创建2) 配置Mock对象的行为(打桩)3) 验证方法调用4) 参数匹配5) 静态方法 2.2 常用注解1) Mock2) BeforeEach 与 BeforeAfter3) InjectMocks4) Spy5) Captor6) RunWi…...

基于SpringBoot+VueJS+微信小程序技术的图书森林共享小程序设计与实现:7000字论文+源代码参考

博主介绍&#xff1a;硕士研究生&#xff0c;专注于信息化技术领域开发与管理&#xff0c;会使用java、标准c/c等开发语言&#xff0c;以及毕业项目实战✌ 从事基于java BS架构、CS架构、c/c 编程工作近16年&#xff0c;拥有近12年的管理工作经验&#xff0c;拥有较丰富的技术架…...

GitHub连接超时问题 Recv failure: Connection was reset

用手机热点WIF拉取git项目的时候&#xff0c;遇到Recv failure: Connection was reset问题。 解决办法 一、手动开启本地代理 二、在终端&#xff08;cmd&#xff09;输入命令 git config --global http.proxy http://127.0.0.1:7890 git config --global https.proxy https:…...

浅谈PostCSS

1. 背景 css的预处理器语言&#xff08;比如 sass&#xff0c; less&#xff0c; stylus&#xff09;的扩展性不好&#xff0c;你可以使用它们已有的功能&#xff0c;但如果想做扩展就没那么容易。 sass是很常用的css预处理器语言&#xff0c;在webpack中要使用它&#xff0c;…...

GCN、GIN

# 使用TuDataset 中的PROTEINS数据集。 # 里边有1113个蛋白质图&#xff0c;区分是否为酶&#xff0c;即二分类问题。# 导包 from torch_geometric.datasets import TUDataset from torch_geometric.data import DataLoader import torch import torch.nn as nn import torch.…...

Web控件进阶交互

Web控件进阶交互 测试时常需要模拟键盘或鼠标操作&#xff0c;可以用Python的ActionChains来模拟。ActionChains是Selenium提供的一个子类&#xff0c;用于生成和执行复杂的用户交互操作&#xff0c;允许将一系列操作链接在一起&#xff0c;然后一次性执行。 from selenium im…...

基于SpringBoot的校园疫情防控系统

你好&#xff0c;我是专注于计算机科学与技术的研究者。如果你对我的工作感兴趣或有任何问题&#xff0c;欢迎随时联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot框架&#xff0c;B/S架构 工具&#xff1a;Eclipse&#xff0c;Mav…...

elasticsearch 查询超10000的解决方案

前言 默认情况下&#xff0c;Elasticsearch集群中每个分片的搜索结果数量限制为10000。这是为了避免潜在的性能问题。 但是我们 在实际工作过程中时常会遇到 需要深度分页&#xff0c;以及查询批量数据更新的情况 问题&#xff1a;当请求form size >10000 时&#xff0c…...

SpringCloud集成kafka集群

目录 1.引入kafka依赖 2.在yml文件配置配置kafka连接 3.注入KafkaTemplate模版 4.创建kafka消息监听和消费端 5.搭建kafka集群 5.1 下载 kafka Apache KafkaApache Kafka: A Distributed Streaming Platform.https://kafka.apache.org/downloads.html 5.2 在config目录下做…...

Macos 远程登录 Ubuntu22.04 桌面

这里使用的桌面程序为 xfce, 而 gnome 桌面则测试失败。 1,安装 在ubuntu上&#xff0c;安装 vnc server与桌面程序xfce sudo apt install xfce4 xfce4-goodies tightvncserver 2&#xff0c;第一次启动和配置 $ tightvncserver :1 设置密码。 然后修改配置&#xff1a…...

第十届MathorCup高校数学建模挑战赛-A题:无车承运人平台线路定价问题

目录 摘 要 1 问题重述 1.1 研究背景 1.2 研究问题 2 符号说明与模型假设 2.1 符号说明 2.2 模型假设 3 问题一:模型建立与求解 3.1 问题分析与思路 3.2 模型建立 3.2.1 多因素回归模型 3.3 模型求解 3.3.1 数据预处理 3.3.2 重要度计算 4 问题二:模型建立与求…...

在分布式环境中,怎样保证 PostgreSQL 数据的一致性和完整性?

文章目录 在分布式环境中保证 PostgreSQL 数据的一致性和完整性一、数据一致性和完整性的重要性二、分布式环境对数据一致性和完整性的挑战&#xff08;一&#xff09;网络延迟和故障&#xff08;二&#xff09;并发操作&#xff08;三&#xff09;数据分区和复制 三、保证 Pos…...

RabbitMq如何保证消息的可靠性和稳定性

RabbitMq如何保证消息的可靠性和稳定性 rabbitMq不会百分之百让我们的消息安全被消费&#xff0c;但是rabbitMq提供了一些机制来保证我们的消息可以被安全的消费。 消息确认 消息者在成功处理消息后可以发送确认&#xff08;ACK&#xff09;给rabbitMq&#xff0c;通知消息已…...

druid(德鲁伊)数据线程池连接MySQL数据库

文章目录 1、druid连接MySQL2、编写JDBCUtils 工具类 1、druid连接MySQL 初学JDBC时&#xff0c;连接数据库是先建立连接&#xff0c;用完直接关闭。这就需要不断的创建和销毁连接&#xff0c;会消耗系统的资源。 借鉴线程池的思想&#xff0c;数据连接池就这么被设计出来了。…...

观察者模式的实现

引言&#xff1a;观察者模式——程序中的“通信兵” 在现代战争中&#xff0c;通信是胜利的关键。信息力以网络、数据、算法、算力等为底层支撑&#xff0c;在现代战争中不断推动感知、决策、指控等各环节产生量变与质变。在软件架构中&#xff0c;观察者模式扮演着类似的角色…...

Eureka: Netflix开源的服务发现框架

在微服务架构中&#xff0c;服务发现是一个关键组件&#xff0c;它允许服务实例之间相互发现并进行通信。Eureka是由Netflix开源的服务发现框架&#xff0c;它是Spring Cloud体系中的核心组件之一。Eureka提供了服务注册与发现的功能&#xff0c;支持区域感知和自我保护机制&am…...

go-基准测试

基准测试 Demo // fib_test.go package mainimport "testing"func BenchmarkFib(b *testing.B) {for n : 0; n < b.N; n {fib(30) // run fib(30) b.N times} }func fib(n int) int {if n 0 || n 1 {return n}return fib(n-2) fib(n-1) }benchmark 和普通的单…...

线性代数|机器学习-P23梯度下降

文章目录 1. 梯度下降[线搜索方法]1.1 线搜索方法&#xff0c;运用一阶导数信息1.2 经典牛顿方法&#xff0c;运用二阶导数信息 2. hessian矩阵和凸函数2.1 实对称矩阵函数求导2.2. 线性函数求导 3. 无约束条件下的最值问题4. 正则化4.1 定义4.2 性质 5. 回溯线性搜索法 1. 梯度…...

SQL,python,knime将数据混合的文字数字拆出来,合并计算实战

将下面将数据混合的文字数字拆出来&#xff0c;合并计算 一、SQL解决&#xff1a; ---创建表插入数据 CREATE TABLE original_data (id INT AUTO_INCREMENT PRIMARY KEY,city VARCHAR(255),value DECIMAL(10, 2) );INSERT INTO original_data (city, value) VALUES (上海0.5…...

mac ssh连接工具

在Mac上&#xff0c;有多个SSH连接工具可供选择&#xff0c;这些工具根据其功能和适用场景的不同&#xff0c;可以满足不同用户的需求。以下是一些推荐的SSH客户端软件&#xff1a;12 iTerm2&#xff1a;这是一款功能强大的终端应用程序&#xff0c;提供了丰富的功能和定制选项…...

conda相比python好处

Conda 作为 Python 的环境和包管理工具&#xff0c;相比原生 Python 生态&#xff08;如 pip 虚拟环境&#xff09;有许多独特优势&#xff0c;尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处&#xff1a; 一、一站式环境管理&#xff1a…...

Cesium1.95中高性能加载1500个点

一、基本方式&#xff1a; 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...

leetcodeSQL解题:3564. 季节性销售分析

leetcodeSQL解题&#xff1a;3564. 季节性销售分析 题目&#xff1a; 表&#xff1a;sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

并发编程 - go版

1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...

GO协程(Goroutine)问题总结

在使用Go语言来编写代码时&#xff0c;遇到的一些问题总结一下 [参考文档]&#xff1a;https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现&#xff1a; 今天在看到这个教程的时候&#xff0c;在自己的电…...

C++ 设计模式 《小明的奶茶加料风波》

&#x1f468;‍&#x1f393; 模式名称&#xff1a;装饰器模式&#xff08;Decorator Pattern&#xff09; &#x1f466; 小明最近上线了校园奶茶配送功能&#xff0c;业务火爆&#xff0c;大家都在加料&#xff1a; 有的同学要加波霸 &#x1f7e4;&#xff0c;有的要加椰果…...

Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement

Cilium动手实验室: 精通之旅---13.Cilium LoadBalancer IPAM and L2 Service Announcement 1. LAB环境2. L2公告策略2.1 部署Death Star2.2 访问服务2.3 部署L2公告策略2.4 服务宣告 3. 可视化 ARP 流量3.1 部署新服务3.2 准备可视化3.3 再次请求 4. 自动IPAM4.1 IPAM Pool4.2 …...

【PX4飞控】mavros gps相关话题分析,经纬度海拔获取方法,卫星数锁定状态获取方法

使用 ROS1-Noetic 和 mavros v1.20.1&#xff0c; 携带经纬度海拔的话题主要有三个&#xff1a; /mavros/global_position/raw/fix/mavros/gpsstatus/gps1/raw/mavros/global_position/global 查看 mavros 源码&#xff0c;来分析他们的发布过程。发现前两个话题都对应了同一…...

Copilot for Xcode (iOS的 AI辅助编程)

Copilot for Xcode 简介Copilot下载与安装 体验环境要求下载最新的安装包安装登录系统权限设置 AI辅助编程生成注释代码补全简单需求代码生成辅助编程行间代码生成注释联想 代码生成 总结 简介 尝试使用了Copilot&#xff0c;它能根据上下文补全代码&#xff0c;快速生成常用…...

P10909 [蓝桥杯 2024 国 B] 立定跳远

# P10909 [蓝桥杯 2024 国 B] 立定跳远 ## 题目描述 在运动会上&#xff0c;小明从数轴的原点开始向正方向立定跳远。项目设置了 $n$ 个检查点 $a_1, a_2, \cdots , a_n$ 且 $a_i \ge a_{i−1} > 0$。小明必须先后跳跃到每个检查点上且只能跳跃到检查点上。同时&#xff0…...