Bootstrap

Java单元测试Mock框架---Mockito入门介绍

Mockito是一个针对Java的单元测试模拟框架,它允许开发者创建和配置mock对象,这些mock对象可以作为复杂依赖项的替代品,使测试更加独立和高效。以下是对Mockito的详细介绍:

一、Mockito 框架介绍

Mockito 是一个基于MIT协议的开源java测试框架。 Mockito区别于其他模拟框架的地方主要是允许开发者在没有建立“预期”时验证被测系统的行为。对mock对象的一个批评是测试代码与被测系统高度耦合,由于Mockito试图通过移除“期望规范”来去除expect-run-verify模式(期望--运行--验证模式),因此使耦合度降低到最低。这样的突出特性简化了测试代码,使它更容易阅读和修改了。

1.1、Mockito的基本概念

1、Mock对象
在调试期间用来作为真实对象的替代品。Mock测试就是在测试过程中,对那些不容易构建或不容易获取的比较复杂的对象,用一个虚拟的对象(Mock对象)来创建以便测试。
2、Mock测试
Mock测试最大的功能是帮助把单元测试的耦合分解开,如果代码对另一个类或者接口有依赖,Mock测试能够模拟这些依赖,并验证所调用的依赖的行为。
3、Stub
存根,即为mock对象的方法指定返回值(可抛出异常)。
4、Verify
行为验证,验证指定方法调用情况(是否被调用、调用次数等)。

1.2、Mockito的主要功能

1、模拟方法的返回值:开发者可以指定mock对象在调用某个方法时返回的特定值。
2、模拟抛出异常:开发者可以设置mock对象在调用某个方法时抛出指定的异常。
3、验证方法被调用次数:Mockito可以验证mock对象的某个方法被调用的次数,是否符合预期。
4、验证方法参数类型:开发者可以验证mock对象的某个方法在调用时,传入的参数类型是否符合预期。
5、捕获方法参数值:Mockito可以捕获mock对象的某个方法在调用时传入的参数值,以便进行进一步断言。

1.3、Mockito的使用方法

1、创建Mock对象:使用Mockito.mock(Class classToMock)方法创建mock对象。
2、设置存根:使用Mockito.when(…)或Mockito.doReturn(…)等方法为mock对象的方法设置返回值或异常。
3、验证行为:使用Mockito.verify(…)方法验证mock对象的交互行为是否符合预期。

1.4、Mockito的注意事项

1、不能Mock静态方法:Mockito无法模拟静态方法的行为。
2、不能Mock private方法:Mockito无法模拟private方法的行为。
3、不能Mock final class:Mockito无法模拟被声明为final的类的行为。

1.5、Mockito的应用场景

Web应用:测试控制器的输入输出,而不关心底层的服务逻辑。
微服务:模拟外部服务的调用,以验证微服务之间的交互。
数据处理:在对接数据库时,模拟数据仓库的行为。

综上所述,Mockito是一个功能强大且灵活的Java单元测试模拟框架,它能够帮助开发者更加高效地进行单元测试,提高代码质量和开发效率。

二、解决的问题(大白话)

我们在写单元测试时,总会遇到类似这些问题:

  1. 构造的入参,对于极值、异常边界场景不好复现,相关的逻辑测不到,只能依靠测试环境或预发跑,运气不好可能要改好几次代码重启机器验证,费时费力;
  2. 依赖别人接口,可能需要别人协助测试环境数据库插数才能跑通;
  3. 依赖的别人的接口还没有开发完,为了不影响提测,如何完成单元测试?
  4. 编写的单元测试依赖测试数据库的数据,每次跑都要数据库改数?
  5. 对service层加了逻辑,跑单元测试本地验证的时候,由于种种原因,本地环境跑不起来,折腾半天跑起来验证完了,下次开发需求又遇到了另一个问题本地环境启动报错???
  6. 我就想dubug到某一行代码,但是逻辑复杂,东拼西凑的参数就是走不到,自己看代码逻辑还要去问别人接口的返回值逻辑??(未完待续……)

在这里插入图片描述 使我们的单测满足AIR原则:Automatic(自动化)、Independent(独立性)、Repeatable(可重复)

简单说就是无论谁的本地环境,无论判断条件多么苛刻,无论本地数据库的测试数据被谁删了改了,无论别人接口的返回值逻辑多复杂,无论自己代码逻辑多复杂,都能独立的、可重复执行的、行级别覆盖的单元测试用例。

三、为什么需要模拟?

在我们一开始学编程时,我们所写的对象通常都是独立的。hello world之类的类并不依赖其他的类(System.out除外),也不会操作别的类。但实际上软件中是充满依赖关系的。我们会基于service类写操作类,而service类又是基于数据访问类(DAOs)的,依次下去。
  在这里插入图片描述
上图的依赖关系
  单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码。这种测试可以让你无视代码的依赖关系去测试代码的有效性。
  核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。
  
  下面的代码就是这样的例子:

import java.util.ArrayList;  
public class Counter {  
     public Counter() {  
     }  
     public int count(ArrayList items) {  
          int results = 0;  
          for(Object curItem : items) {  
               results ++;  
          }  
          return results;  
     }  
} 

如你所见,上面的例子十分简单,但它阐明了要点。当你想要测试count方法时,你会针对count方法本身如何工作去写测试代码。你不会去测试ArrayList是否正常工作,因为你默认它已经被测过并且工作正常。你唯一的目标就是测试对ArrayList的使用。
  模拟对象的概念就是我们想要创建一个可以替代实际对象的对象。这个模拟对象要可以通过特定参数调用特定的方法,并且能返回预期结果。

四、模拟有哪些关键点?

在谈到模拟时,你只需关心三样东西:设置测试数据,设定预期结果,验证结果。
一些单元测试方案根本就不涉及这些,有的只涉及设置测试数据,有的只涉及设定预期结果和验证。

Stubbing (桩)
  Stubbing就是告诉fake当与之交互时执行何种行为过程。通常它可以用来提供那些测试所需的公共属性(像getters和setters)和公共方法。
  当谈到stubbing方法,通常你有一系列的选择。或许你希望返回一个特殊的值,抛出一个错误或者触发一个事件,此外,你可能希望指出方法被调用时的不同行为(即通过传递匹配的类型或者参数给方法)。
  这咋一听起来工作量很大,但通常并非这样。许多mocking框架的一个重要功能就是你不需要提供stub 的实体方法,也不用在执行测试期间stub那些未被调用的方法或者未使用的属性。
  
设置预期
  Fake的一个关键的特性就是当你用它进行模拟测试时你能够告诉它你预期的结果。
  例如,你可以要求一个特定的函数被准确的调用3次,或不被调用,或调用至少两次但不超过5次,或者需要满足特定类型的参数、特定值和以上任意的组合的调用。可能性是无穷的。
  通过设定预期结果告诉fake你期望发生的事情。因为它是一个模拟测试,所以实际上什么也没发生。但是,对于被测试的类来说,它并无法区分这种情况。所以fake能够调用函数并让它做它该做的。值得注意的是,大多数模拟框架除了可以创建接口的模拟测试外,还可以创建公有类的模拟测试。

验证预期结果
  设置预期和验证预期是同时进行的。设置预期在调用测试类的函数之前完成,验证预期则在它之后。所以,首先你设定好预期结果,然后去验证你的预期结果是否正确。
  在一个单元测试中,如果你设定的预期没有得到满足,那么这个单元测试就是失败了。例如,你设置预期结果是 ILoginService.login函数必须用特定的用户名和密码被调用一次,但是在测试中它并没有被调用,这个fake没被验证,所以测试失败。

五、案例

5.1、验证某些行为

    /**
     * 验证某些行为
     * 一旦mock对象被创建了,mock对象会记住所有的交互,然后你就可能选择性的验证你感兴趣的交互
     */
    @Test
    public void test1() {
        // 创建mock对象
        List<String> mockedList = mock(List.class);
        // 使用mock对象
        mockedList.add("one");
        mockedList.clear();
        // verify 用于确保模拟对象上的特定方法被调用
        verify(mockedList).add("one");
        verify(mockedList).clear();
    }

5.2、如何做一些测试桩

    /**
     * 如何做一些测试桩
     * <p>
     * 测试桩(Stub):
     * 测试桩是指为某个方法调用提供预定义返回值的代码。它通常用于控制测试中的代码路径,
     * 使测试能够按照预期进行,而不依赖于外部系统或真实对象的复杂行为。
     * <p>
     * 打桩(Stubbing):
     * 打桩是指设置测试桩的过程,即定义Mock对象在被调用时应如何响应。
     * 通过打桩,可以模拟出不同的测试场景,以便验证代码在不同情况下的行为。
     */
    @Test
    public void test2() {
        // 创建mock对象
        LinkedList<String> mockedList = mock(LinkedList.class);
        // 测试桩
        when(mockedList.get(0)).thenReturn("first");
        when(mockedList.get(1)).thenThrow(new RuntimeException());
        // 输出“first”
        System.out.println(mockedList.get(0));
        // 因为get(999) 没有打桩,因此输出null
        System.out.println(mockedList.get(999));
        // 抛出异常
        System.out.println(mockedList.get(1));
    }

5.3、参数匹配器 (matchers)

    /**
     * 参数匹配器 (matchers)
     */
    @Test
    public void test3() {
        LinkedList<String> mockedList = mock(LinkedList.class);
        // 使用内置的anyInt()参数匹配器:用于匹配任意整数类型的参数
        when(mockedList.get(anyInt())).thenReturn("element");
        // 输出element
        System.out.println(mockedList.get(999));
        // 验证参数匹配器
        verify(mockedList).get(anyInt());


        // 定义自定义的参数匹配器,使用匿名内部类
        ArgumentMatcher<String> startsWithAMatcher = name -> {
            // 在这里定义匹配逻辑:姓名为:张三
            return name != null && name.equals("张三");
        };
        // 创建Greeter的模拟对象
        Greeter greeter = mock(Greeter.class);
        // 设置模拟对象的行为:当调用greet方法且参数匹配时,返回一个特定的问候语
        when(greeter.greet(argThat(startsWithAMatcher))).thenReturn("Hello, 张三!");
        // 调用greet方法,并传入一个匹配的参数
        String result = greeter.greet("张三");
        System.out.println(result);
        // 验证结果和调用
        assertEquals("Hello, 张三!", result);
        verify(greeter).greet(argThat(startsWithAMatcher));

    }

    // 假设的Greeter接口
    interface Greeter {
        String greet(String name);
    }

5.4、检查模拟对象上是否还有未验证的交互

    /**
     * 检查模拟对象上是否还有未验证的交互
     */
    @Test
    public void test4() {
        // 创建mock对象
        List<String> mockedList = mock(List.class);
        mockedList.add("one");
        mockedList.add("two");
        verify(mockedList).add("one");
        // 检查模拟对象上是否还有未验证的交互
        verifyNoMoreInteractions(mockedList);
    }

5.5、为连续的调用做测试桩

    /**
     * 为连续的调用做测试桩
     */
    @Test
    public void test5() {
        // 创建Greeter的模拟对象
        Greeter greeter = mock(Greeter.class);
        when(greeter.greet("张三"))
                .thenReturn("你好,张三")
                .thenReturn("你好,张三2")
                .thenThrow(new RuntimeException());

        // 第一次调用 : 你好,张三
        System.out.println(greeter.greet("张三"));
        // 返回null
        System.out.println(greeter.greet("李四"));
        // 第二次调用 : 你好,张三2
        System.out.println(greeter.greet("张三"));
        // 后续调用 : 抛出运行时异常
        System.out.println(greeter.greet("张三"));
    }
    
    // 假设的Greeter接口
    interface Greeter {
        String greet(String name);
    }

5.6、模拟实际场景

假设有个处理订单的接口orderService.processOrder,里面有inventoryService.isStockAvailable校验库存的方法,而这个方法是调用外部ERP系统去校验的,测试起来会比较麻烦,因此我们想绕过这个校验逻辑,使得测试变得简单,所以我们可以把inventoryService进行模拟。

订单接口 OrderService

public interface OrderService {
    boolean processOrder(Record record);
}

订单接口实现 OrderServiceImpl

public class OrderServiceImpl implements OrderService {

    private final InventoryService inventoryService;

    public OrderServiceImpl(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }

    /**
     * 处理订单
     *
     * @param record productId 产品ID
     * @param record quantity  库存数量
     */
    @Override
    public boolean processOrder(Record record) {
        String productId = record.getStr("productId");
        Integer quantity = record.getInt("quantity");
        System.out.println("产品ID:" + productId);
        System.out.println("库存数量:" + quantity);

        try {
            if (inventoryService.isStockAvailable(record)) {
                System.out.println("订单处理成功");
                return true;
            } else {
                System.out.println("库存不足");
                return false;
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return false;
        }
    }
}

库存接口 InventoryService

public interface InventoryService {
    boolean isStockAvailable(Record record);
}

库存接口实现 InventoryServiceImpl

public class InventoryServiceImpl implements InventoryService {

    /**
     * 校验库存(调用外部ERP数据)
     *
     * @param record productId 产品ID
     * @param record quantity  库存数量
     */
    @Override
    public boolean isStockAvailable(Record record) {
    	// 如果调用进入,打印语句
        System.out.println("进入校验库存方法");
        return false;
    }
}

mock单元测试

public class MockTestOrder {

    private InventoryService inventoryServiceMock;
    private OrderServiceImpl orderService;

    // 在@Test标注的测试方法之前运行
    @Before
    public void setUp() {
        // 初始化 Mockito 模拟对象
        inventoryServiceMock = Mockito.mock(InventoryService.class);
        // 使用模拟对象创建 OrderServiceImpl 实例
        orderService = new OrderServiceImpl(inventoryServiceMock);
    }

    @Test
    public void test1() {
        Record record = new Record();
        record.set("productId", 123);
        record.set("quantity", 5);
        // 设置Mock对象的行为,模拟库存充足的情况
        when(inventoryServiceMock.isStockAvailable(record)).thenReturn(true);

        // 调用被测试的方法
        boolean result = orderService.processOrder(record);

        // 验证结果是否符合预期
        assertTrue(result);

        // 验证Mock对象的方法是否被调用,并且参数是否正确
        verify(inventoryServiceMock).isStockAvailable(record);
    }

    @Test
    public void test2() {
        Record record = new Record();
        record.set("productId", 123);
        record.set("quantity", 10);
        // 设置Mock对象的行为,模拟库存不足的情况
        when(inventoryServiceMock.isStockAvailable(record)).thenReturn(false);

        // 调用被测试的方法
        boolean result = orderService.processOrder(record);

        // 验证结果是否符合预期
        assertFalse(result);

        // 验证Mock对象的方法是否被调用,并且参数是否正确
        verify(inventoryServiceMock).isStockAvailable(record);

        // 得到一个抓取器
        ArgumentCaptor<Record> recordCaptor = ArgumentCaptor.forClass(Record.class);
        // 验证模拟对象的isStockAvailable()是否被调用一次,并抓取调用时传入的参数值
        verify(inventoryServiceMock).isStockAvailable(recordCaptor.capture());
        // 获取抓取到的参数值
        Record saveRecord = recordCaptor.getValue();
        // 验证调用时的参数值
        assertEquals("10", saveRecord.getStr("quantity"));
        // 检查模拟对象上是否还有未验证的交互
        verifyNoMoreInteractions(inventoryServiceMock);
    }

    @Test(expected = RuntimeException.class)
    public void test3() {
        Record record = new Record();
        record.set("productId", 123);
        record.set("quantity", 10);
        // 设置Mock对象的行为,模拟InventoryService抛出异常的情况
        when(inventoryServiceMock.isStockAvailable(record)).thenThrow(new RuntimeException("获取库存异常"));

        // 调用被测试的方法,并期望抛出RuntimeException
        orderService.processOrder(record);
    }
}

test1模拟库存充足的情况
执行结果

产品ID:123
库存数量:5
订单处理成功

test2模拟库存不足的情况
执行结果

产品ID:123
库存数量:10
库存不足

test3模拟InventoryService抛出异常的情况
执行结果

在这里插入图片描述

可以看到以上案例并没有打印出:"进入校验库存方法",所以InventoryService是mock成功的,从而绕过了库存校验。

本次文章就写到这,如果觉得不错,可以点赞+收藏或者关注下博主。感谢你的阅读!祝你未来可期!
;