• 单元测试Mockito笔记


    单元测试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.0modelVersion>
    
      <groupId>com.ucarinc.frameworkgroupId>
      <artifactId>demo1artifactId>
      <version>1.0-SNAPSHOTversion>
      <packaging>jarpackaging>
    
      <name>demo1name>
      <url>http://maven.apache.orgurl>
    
      <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
      properties>
    
      <dependencies>
        <dependency>
          <groupId>org.slf4jgroupId>
          <artifactId>slf4j-apiartifactId>
          <version>2.0.13version>
        dependency>
        <dependency>
          <groupId>org.slf4jgroupId>
          <artifactId>slf4j-log4j12artifactId>
          <version>2.0.13version>
        dependency>
    
        <dependency>
          <groupId>org.junit.jupitergroupId>
          <artifactId>junit-jupiterartifactId>
          <version>5.10.2version>
          <scope>testscope>
        dependency>
    
        <dependency>
          <groupId>org.projectlombokgroupId>
          <artifactId>lombokartifactId>
          <version>1.18.32version>
        dependency>
        <dependency>
          <groupId>org.mockitogroupId>
          <artifactId>mockito-coreartifactId>
          <version>5.11.0version>
          <scope>testscope>
        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 {
        @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");
        }
    }  
    
    

    总结

    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;
            }
        }
    
        @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();
        }
    }
    
    

    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{
        @Test
        public void test01() {
    
            // 使用Mockito模拟一个Random对象
            Random random = Mockito.mock(Random.class);
            // 调用nextInt()方法,输出随机数,因random 行为为进行打桩,故输出默认值0(random.nextInt() 返回的是int类型)
            System.out.println("第一次:"+random.nextInt());
            // 验证random.nextInt()这个方法是否只调用了一次
            verify(random).nextInt();
            // 指定当调用nextInt()时,始终返回1
            Mockito.when(random.nextInt()).thenReturn(1);
            System.out.println("第二次:"+random.nextInt()); // 再次调用nextInt(),输出应为1
            // 断言nextInt()方法返回值是否为1
            Assertions.assertEquals(1,random.nextInt());
            // 验证nextInt()方法是否被调用了两次
            verify(random, times(3)).nextInt();
    
    
    
        }
    }
    

    4) 参数匹配

    Mockito提供了多种参数匹配器(Matchers)用于更灵活的验证和配置行为:

    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.mockitogroupId>
          <artifactId>mockito-inlineartifactId>
          <version>5.2.0version>
          <scope>testscope>
        dependency>
        
        <dependency>
          <groupId>org.apache.commonsgroupId>
          <artifactId>commons-lang3artifactId>
          <version>3.14.0version>
        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"));
    
        }
    
    
    }
    
    

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

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

    2) @BeforeEach 与 @BeforeAfter

    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("==============测试后结果===============");
        }
    
    
    
    }
    
    

    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 {
    
        @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;
    

    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;
            }
        }
    
    
        @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 对象打桩,因此返回默认值

    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.0modelVersion>
    
      <groupId>com.ucarinc.frameworkgroupId>
      <artifactId>demo1artifactId>
      <version>1.0-SNAPSHOTversion>
      <packaging>jarpackaging>
    
      <name>demo1name>
      <url>http://maven.apache.orgurl>
    
      <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
      properties>
    
      <dependencies>
        <dependency>
          <groupId>org.slf4jgroupId>
          <artifactId>slf4j-apiartifactId>
          <version>2.0.13version>
        dependency>
        <dependency>
          <groupId>org.slf4jgroupId>
          <artifactId>slf4j-log4j12artifactId>
          <version>2.0.13version>
        dependency>
    
        <dependency>
          <groupId>org.junit.jupitergroupId>
          <artifactId>junit-jupiterartifactId>
          <version>5.10.2version>
          <scope>testscope>
        dependency>
    
        <dependency>
          <groupId>org.projectlombokgroupId>
          <artifactId>lombokartifactId>
          <version>1.18.32version>
        dependency>
        <dependency>
          <groupId>org.mockitogroupId>
          <artifactId>mockito-coreartifactId>
          <version>5.11.0version>
          <scope>testscope>
        dependency>
    
        <dependency>
          <groupId>org.mockitogroupId>
          <artifactId>mockito-inlineartifactId>
          <version>5.2.0version>
          <scope>testscope>
        dependency>
        
        <dependency>
          <groupId>org.apache.commonsgroupId>
          <artifactId>commons-lang3artifactId>
          <version>3.14.0version>
        dependency>
        <dependency>
          <groupId>org.mockitogroupId>
          <artifactId>mockito-junit-jupiterartifactId>
          <version>5.11.0version>
          <scope>testscope>
        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);
        }
    
    
    
    
    }
    

    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实例:

        @Mock
        List<String> mockedList;
    
        @Captor
        ArgumentCaptor<String> argCaptor;
    
        @BeforeEach
        void setUp() {
            MockitoAnnotations.openMocks(this);
        }
    
        @Test
        public void whenUseCaptorAnnotation_thenTheSame() {
            mockedList.add("one");
            verify(mockedList).add(argCaptor.capture());
            assertEquals("one", argCaptor.getValue());
        }
    

    6) @RunWith和@ExtendWith

    测试类上使用 @RunWith(SpringRunner.class) 注解(使用的是 JUnit 4)
    测试类上使用 @ExtendWith(SpringExtension.class)注解(使用的是 JUnit 5)

    SpringBoot2.4.x之后,改为默认仅集成JUnit5,干掉了兼容JUnit4

    @RunWith
    • @RunWith就是一个运行器
    • @RunWith(JUnit4.class)就是指用JUnit4来运行
    • @RunWith(SpringJUnit4ClassRunner.class),让测试运行于Spring测试环境,以便在测试开始的时候自动创建Spring的应用上下文
    @RunWith(SpringRunner.class) //14.版本之前用的是SpringJUnit4ClassRunner.class
    @SpringBootTest(classes = Application.class) //1.4版本之前用的是//@SpringApplicationConfiguration(classes = Application.class)
    public class SystemInfoServiceImplTest {
    
            @Autowired
            private ISystemInfoService systemInfoservice;
    
            @Test
            public void add() throws Exception {
            }
    
            @Test
             public void findAll() throws Exception {
             }
    
    }
    
    
    @ExtendWith

    @ExtendWith 具体Demo展示如下:

    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 {
        // 测试代码
    }
    

    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 项目。

    
    <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.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>3.3.1version>
            <relativePath/> 
        parent>
    
        <modelVersion>4.0.0modelVersion>
        <groupId>com.lkcoffee.frameworkgroupId>
        <artifactId>demo2artifactId>
        <version>0.0.1-SNAPSHOTversion>
        <name>demo2name>
        <description>demo2description>
        <properties>
            <java.version>17java.version>
            <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
            <spring-boot.version>3.3.1spring-boot.version>
        properties>
    
        <dependencies>
    
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-spring-boot3-starterartifactId>
                <version>3.5.7version>
            dependency>
    
            
            <dependency>
                <groupId>com.mysqlgroupId>
                <artifactId>mysql-connector-jartifactId>
                <scope>runtimescope>
                <version>8.3.0version>
            dependency>
    
            <dependency>
                <groupId>org.apache.commonsgroupId>
                <artifactId>commons-lang3artifactId>
            dependency>
    
    
            <dependency>
                <groupId>cn.hutoolgroupId>
                <artifactId>hutool-allartifactId>
                <version>5.8.28version>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <optional>trueoptional>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-compiler-pluginartifactId>
                    <version>3.8.1version>
                    <configuration>
                        <source>17source>
                        <target>17target>
                        <encoding>UTF-8encoding>
                    configuration>
                plugin>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                    <version>${spring-boot.version}version>
                    <configuration>
                        <mainClass>com.lkcoffee.framework.demo2.Demo2ApplicationmainClass>
                        <skip>trueskip>
                    configuration>
                    <executions>
                        <execution>
                            <id>repackageid>
                            <goals>
                                <goal>repackagegoal>
                            goals>
                        execution>
                    executions>
                plugin>
            plugins>
        build>
    
    project>
    
    

    添加application.yml

    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> {
    
    }
    

    编写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 {
    
    
        @Override
        public List<User> queryAll() {
            log.info("被真实调用了, 执行了 查询所有用户信息");
            return list();
        }
    
        @Override
        public User queryById(Long id) {
            log.info("被真实调用了, 根据用户id:{} 查询用户",id);
            return getById(id);
        }
    
        @Transactional(rollbackFor = Exception.class)
        @Override
        public Boolean addUser(User user) {
            log.info("被真实调用了, 添加用户信息:{}",user);
            if(Objects.nonNull(user.getId())){
                throw new RuntimeException("被真实调用了,新增用户,id应该为空");
            }
            if(Objects.isNull(user.getAge()) || user.getAge() < 0 || user.getAge() > 100){
                throw new RuntimeException("被真实调用了,请填写正确的年龄");
            }
            if(StringUtils.isBlank(user.getName())){
                throw new RuntimeException("被真实调用了,对不起,姓名不能为空");
            }
    
            return save(user);
        }
    
        @Transactional(rollbackFor = Exception.class)
        @Override
        public Integer updateUser(User user) {
            System.out.println("执行了真实的更新用户方法");
            int result= getBaseMapper().updateById(user);
            System.out.println("update user result:"+result);
            return result;
        }
    
    
    }
    
    

    编写controller

    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
    

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

    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 {
    
        @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对象来模拟外部依赖,从而实现单元测试的独立性和可重复性。

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

    3) 打桩

    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
    

    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 {
    
        @Mock
        private List<Integer> mockList;
    
    
        @Test
        public void test1() {
            //第1次调用返回1,第2次调用返回2,第3次及之后的调用都返回3
            // when(mockList.size()).thenReturn(1).thenReturn(2).thenReturn(3),
            // 可简写为:
            when(mockList.size()).thenReturn(1, 2, 3);
            Assertions.assertEquals(1, mockList.size());
            Assertions.assertEquals(2, mockList.size());
            Assertions.assertEquals(3, mockList.size());
            Assertions.assertEquals(3, mockList.size());
        }
    }
    
    

    5) 实战

    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);
        }
    }
    

    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 {
    	@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================");
    	}
    }
    
    
  • 相关阅读:
    第四天:职场文科宝妈动手实操练习PYTHON编程基础语法,能成功吗
    【运维心得】ftp命令行使用
    linux 设置默认启动图形化界面
    【ARM Coresight Debug 系列 16 -- Linux 断点 BRK 中断使用详细介绍】
    LeetCode 1769. 移动所有球到每个盒子所需的最小操作数 -- 前缀和
    巧用自定义注解,一行代码搞定审计日志
    如何使用git-credentials来管理git账号
    虚拟机无法启动提示give root password for maintenance的多种解决方法
    OpenSSL 编程 一:基本概念
    uniapp 下载文件到手机
  • 原文地址:https://blog.csdn.net/wujing1_1/article/details/140383761