单元测试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 {
    @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.mockito</groupId>
      <artifactId>mockito-inline</artifactId>
      <version>5.2.0</version>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.14.0</version>
    </dependency>

如果jdk版本低的话,版本可以低一点.

使用 mockStatic() 方法来 mock静态方法的所属类,此方法返回一个具有作用域的模拟对象。

    @Test
    public void testJoinWith() {

        // 使用 Mockito 框架模拟 StringUtils 类的静态方法
        MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);

        // 创建一个字符串列表,作为 joinWith 方法的输入参数
        List<String> stringList = Arrays.asList("a", "b", "c");

        // 配置模拟行为,当调用 StringUtils.joinWith(",", stringList) 时,返回 "a,b,c"
        stringUtilsMockedStatic.when(() -> StringUtils.joinWith(",", stringList)).thenReturn("a,b,c");

        // 断言验证模拟行为是否正确,即 joinWith 方法返回的字符串是否与预期的 "a,b,c" 相等
        Assertions.assertTrue(StringUtils.joinWith(",", stringList).equals("a,b,c"));

    }

但是如果你写成下面这样子的话,会发送报错

package com.ucarinc.framework;

import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import java.util.Arrays;
import java.util.List;


class Demo2ApplicationTests {

    @Test
    public void testJoinWith() {

        // 使用 Mockito 框架模拟 StringUtils 类的静态方法
        MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);

        // 创建一个字符串列表,作为 joinWith 方法的输入参数
        List<String> stringList = Arrays.asList("a", "b", "c");

        // 配置模拟行为,当调用 StringUtils.joinWith(",", stringList) 时,返回 "a,b,c"
        stringUtilsMockedStatic.when(() -> StringUtils.joinWith(",", stringList)).thenReturn("a,b,c");

        // 断言验证模拟行为是否正确,即 joinWith 方法返回的字符串是否与预期的 "a,b,c" 相等
        Assertions.assertTrue(StringUtils.joinWith(",", stringList).equals("a,b,c"));

    }


    /**
     * 测试StringUtils类中的join方法。
     * 该测试使用Mockito框架来模拟静态方法的行为,验证join方法是否按照预期工作。
     * */
    @Test
    public void testJoin() {

        // 使用Mockito模拟StringUtils类的静态方法
        MockedStatic<StringUtils> stringUtilsMockedStatic = Mockito.mockStatic(StringUtils.class);

        // 创建一个字符串列表作为join方法的输入
        List<String> stringList = Arrays.asList("a", "b", "c");
        // 配置模拟行为,当调用StringUtils.join(",", stringList)时,返回字符串"a,b,c"
        stringUtilsMockedStatic.when(() -> StringUtils.join(",", stringList)).thenReturn("a,b,c");

        // 断言验证模拟行为是否正确,即 join 方法返回的字符串是否与预期的 "a,b,c" 相等
        Assertions.assertTrue(StringUtils.join(",", stringList).equals("a,b,c"));

    }


}

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

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.0</modelVersion>

  <groupId>com.ucarinc.framework</groupId>
  <artifactId>demo1</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>demo1</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>2.0.13</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>2.0.13</version>
    </dependency>

    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.10.2</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.32</version>
    </dependency>
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>5.11.0</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-inline</artifactId>
      <version>5.2.0</version>
      <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.14.0</version>
    </dependency>
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-junit-jupiter</artifactId>
      <version>5.11.0</version>
      <scope>test</scope>
    </dependency>


  </dependencies>


</project>

代码测试

package com.ucarinc.framework;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;


@ExtendWith(MockitoExtension.class)
public class App4Test {

    private final Logger log= LoggerFactory.getLogger(App4Test.class);

    public static class MockitoTestController{
        public int add(int a, int b){
            System.out.println("测试了a+b  a="+a+",b="+b);
            return a+b;
        }
    }

    @Spy
    private MockitoTestController mockitoTestController;


    @BeforeEach
    void setUp() {

    }

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




}

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

<?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: 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================");
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/795923.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

window下tqdm进度条

原代码是linux下运行&#xff0c;修改后可在window下运行。 #ifndef TQDM_H #define TQDM_H#include <chrono> #include <ctime> #include <numeric> #include <ios> #include <string> #include <cstdlib> #include <iostream> #i…

MacOS 通过Docker安装宝塔面板搭建PHP开发环境

1、docker拉取ubuntu系统 docker pull ubuntu2、运行容器 docker run -i -t -d --name bt -p 20:20 -p 21:21 -p 80:80 -p 443:443 -p 888:888 -p 8888:8888 -p 3306:3306 -p 6379:6379 --privilegedtrue -v /Users/oi/Sites:/www/wwwroot ubuntu-v 后的 /Users/oi/Sites 代表…

分布式系统—Ceph块存储系统(RBD接口)

目录 一、服务端操作 1 创建一个名为 rbd-xy101 的专门用于 RBD 的存储池 2 将存储池转换为 RBD 模式 3 初始化存储池 4 创建镜像 5 管理镜像 6.Linux客户端使用 在管理节点创建并授权一个用户可访问指定的 RBD 存储池 ​编辑修改RBD镜像特性&#xff0c;CentOS7默认情…

fastadmin 如何通过权限组来控制列的显示与隐藏

方法1 以版本控制&#xff08;application/admin/controller/Version.php&#xff09;为例子 需求 就是在有时候&#xff0c;有些列不想让这个权限组的人看到&#xff0c;只给制定的权限组的人看 1.给权限组创建一个字段 ALTER TABLE lt_auth_group ADD COLUMN isBoothView T…

RMAN备份与还原

进入 rman 工具 rman target / 查看 rman 配置 rman> show all; 修改rman 配置 数据库全备 rman> run {allocate channel c1 type disk;allocate channel c2 type disk;backup incremental level 0 database format /home/oracle/backup/full_%d_%s_%t.bak;sql alte…

连接与隔离:Facebook在全球化背景下的影响力

在当今全球化的背景下&#xff0c;Facebook作为全球最大的社交网络平台&#xff0c;不仅连接了世界各地的人们&#xff0c;还在全球社会、经济和文化中发挥着深远的影响。本文将深入探讨Facebook在全球化进程中的作用&#xff0c;以及其对个体和社会之间连接与隔离的双重影响。…

C/C++ 进阶(7)模拟实现map/set

个人主页&#xff1a;仍有未知等待探索-CSDN博客 专题分栏&#xff1a;C 一、简介 map和set都是关联性容器&#xff0c;底层都是用红黑树写的。 特点&#xff1a;存的Key值都是唯一的&#xff0c;不重复。 map存的是键值对&#xff08;Key—Value&#xff09;。 set存的是键…

我的世界1.21多种服务端开服教程,原版/Forge/Fabric/Paper/Mohist...,Minecraft开服教程

Minecraft&#xff08;MC&#xff09;1.21版多种服务端开服教程&#xff0c;我的世界1.21服务器搭建教程&#xff0c;MC原版/Forge/Fabric/Paper/Mohist服务端搭建教程&#xff0c;我的世界MOD/插件服开服教程。 本教程使用 Linux系统MCSManager 面板来搭建Minecraft服务器。 …

每天一个数据分析题(四百二十七)- 方差分析

下面是一个方差分析表&#xff1a; 表中A&#xff0c;B&#xff0c;C&#xff0c;D&#xff0c;E五个单元格内的数据分别是&#xff08; &#xff09;。 A. 40&#xff0c;5&#xff0c;35&#xff0c;60&#xff0c;1.71 B. 40&#xff0c;5&#xff0c;35&#xff0c;60&a…

ES 慢上游响应问题优化在用户体验场景中的实践

在抖音亿级日活流量的情况下&#xff0c;每天收到的用户反馈也是大量的&#xff0c;而用户反馈对于产品的发展与未来是至关重要的&#xff0c;因此用户体验管理平台&#xff08;简称VoC&#xff09;就应运而生&#xff0c;VoC 平台旨在通过技术平台化的方式&#xff0c;结合反馈…

Spring系统学习 - Spring事务的概念

提到事务&#xff0c;这个我们应该比较熟悉了&#xff0c;在数据库学习的过程中&#xff0c;我们或多或少接触过了事务&#xff0c;当然你可能没有用到&#xff0c;也可能用到了&#xff0c;这篇博客我们将围绕Spring的相关事务的概念进行&#xff0c;了解Spring中的事务和事务…

ChatGPT Mac App 发布!

2024 年 6 月&#xff0c;OpenAI 的大语言模型 ChatGPT 的 Mac 客户端与 ChatGPT-4o 一起发布了。ChatGPT Mac 户端可以让用户直接在 Mac 电脑上使用 ChatGPT 进行对话。它提供了一个简单易用的用户界面&#xff0c;用户可以在其中输入文本或语音指令&#xff0c;并接收模型生成…

JavaDS —— 栈 Stack 和 队列 Queue

栈的概念 栈是一种先进后出的线性表&#xff0c;只允许在固定的一端进行插入和删除操作。 进行插入和删除操作的一端被称为栈顶&#xff0c;另一端被称为栈底 栈的插入操作叫做进栈/压栈/入栈 栈的删除操作叫做出栈 现实生活中栈的例子&#xff1a; 栈的模拟实现 下面是Jav…

对照ui图进行大屏幕适配,echerts适配

1.先找到ui图&#xff0c;我这边是1920*1080的屏幕进行的设计 2.在界面找到跟样式的字体大小&#xff0c;进行设置&#xff0c;一般ui设置字体大小便可 3.在js中写入原生js代码 function adapter() {//获取布局视口宽度&#xff0c;布局视口设备横向独立像素值const dpWidth…

在 PostgreSQL 里如何处理数据的归档和清理策略的优化?

文章目录 在 PostgreSQL 中处理数据归档和清理策略的优化一、理解数据归档和清理的重要性二、确定归档和清理的标准三、PostgreSQL 中的数据归档方法&#xff08;一&#xff09;使用分区表&#xff08;二&#xff09;导出数据 四、PostgreSQL 中的数据清理方法&#xff08;一&a…

web3.0的业务场景分析

Web3.0作为互联网的下一个阶段&#xff0c;其核心特点是去中心化、自治、安全和透明。相比于Web2.0&#xff0c;Web3.0将用户数据的所有权和控制权归还给用户&#xff0c;并通过区块链技术等手段确保数据的安全和透明。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发…

Uniapp鸿蒙项目实战

Uniapp鸿蒙项目实战 24.7.6 Dcloud发布了uniapp兼容鸿蒙的文档&#xff1a;Uniapp开发鸿蒙应用 在实际使用中发现一些问题&#xff0c;开贴记录一下 设备准备 windows电脑准备&#xff08;家庭版不行&#xff0c;教育版、企业版、专业版也可以&#xff0c;不像uniapp说的只有…

LeetCode - #93 复原 IP 地址

文章目录 前言1. 描述2. 示例3. 答案关于我们 前言 本题由于没有合适答案为以往遗留问题&#xff0c;最近有时间将以往遗留问题一一完善。 我们社区陆续会将顾毅&#xff08;Netflix 增长黑客&#xff0c;《iOS 面试之道》作者&#xff0c;ACE 职业健身教练。&#xff09;的 Sw…

kotlin数据容器

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 容器是用于存放数据的载体。容器分为数组、集合。 Kotlin作为一门全新的语言&#xff0c;肯定还是要有自己的容…

html设计(两种常见的充电效果)

第一种 完整代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title&…