Java

单元测试 Mockito

2020-11-22  本文已影响0人  虐心笔记

一、前言

之前接触到一次SaaS项目,在进行Dubbo接口测试时,由于某些API是回调接口通过MQ交互传递消息的,走的异步处理。因为MQ在消费消息的时候可能存在延迟,在自动化用例调用接口步骤之后,立即查询数据库结果可能会存在消息还在消费中数据并没有落库,导致断言case会失败,而接口本身定义返回的 message 对于assert没有实际的意义,这就造成了在assert时候的麻烦。因为你不知道MQ消息何时才消费完成并落库,导致用例的不确定性影响通过率。刚开始的思路是通过多次遍历查询数据库,但是结果并不是很理想,因为遍历等待的时间会严重影响用例执行的效率,通过率也不符合预期。

针对以上问题,经过考虑是否能够采用mock的方式来解决。上网查询相关资料,发现 Mockito+PowerMockito 框架,提供的相关API很好的解决了以上问题,不得不说 Mockito 是真的好用,强烈推荐真香!


二、Mockito 入门

POM依赖
  <dependencies>
    <!-- https://mvnrepository.com/artifact/org.testng/testng -->
    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>7.1.0</version>
      <scope>test</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/org.powermock/powermock-api-mockito2 -->
    <dependency>
      <groupId>org.powermock</groupId>
      <artifactId>powermock-api-mockito2</artifactId>
      <version>2.0.9</version>
    </dependency>

  </dependencies>
模拟对象
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.此时调用getName方法,会返回null,因为还没有对方法调用的返回值做Mock
        System.out.println(person.getName());
模拟对象返回值
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.Mock 调用person对象的getName()方法时,返回angst,在给特定的方法调用返回固定值在官方说法中称为 stub 测试桩
        when(person.getName()).thenReturn("angst");
        // 3.此时打印输出 angst
        System.out.println(person.getName());
模拟指定异常
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.模拟调用getAge()方法时,抛出 RuntimeException
        when(person.getAge()).thenThrow(new RuntimeException());
        // 3.此时将会抛出 RuntimeException
        System.out.println(person.getAge());
为返回值为void的函数通过Stub抛出异常
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.用于为无返回值的函数打桩
        doThrow(new RuntimeException("Custom Exception")).when(person).setAge(18);
        // 3.调用这句代码会抛出异常
        person.setAge(18);
参数匹配器 (matchers)
        // 1.Mock Person Object
        LinkedList mockedList = mock(LinkedList.class);
        // 使用内置的anyInt()参数匹配器,anyInt代表任意int
        when(mockedList.get(anyInt())).thenReturn("anyInt");
        //following prints "anyInt"
        System.out.println(mockedList.get(999));
        //you can also verify using an argument matcher
        verify(mockedList).get(anyInt());
验证调用次数
        // 1.Mock Person Object
        Person person = mock(Person.class);
        person.setName("amy");
        person.setName("angst");person.setName("angst");
        person.setName("thin bamboo");person.setName("thin bamboo");person.setName("thin bamboo");
        /* 2.下面两个写法验证效果一样,均验证 setName() 方法是否被调用了一次,verify 函数默认验证的是执行了times(1)
        也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了,如果 times=2 则抛出异常 TooLittleActualInvocations*/
        verify(person).setName("amy");
        verify(person, times(1)).setName("amy");
        // 3.使用never()进行验证,never相当于times(0)
        verify(person, never()).setAge(18);
        //atLeastOnce相当于times(1)
        verify(person, atLeastOnce()).setName("amy");
        //atLeast至少调用N次
        verify(person, atLeast(2)).setName("angst");
        //atLeast最多调用N次
        verify(person, atMost(3)).setName("thin bamboo");
验证执行执行顺序
        // A. Single mock whose methods must be invoked in a particular order
        List singleMock = mock(List.class);
        //using a single mock
        singleMock.add("was added first");
        singleMock.add("was added second");
        //create an inOrder verifier for a single mock
        InOrder inOrder = inOrder(singleMock);
        //following will make sure that add is first called with "was added first, then with "was added second"
        inOrder.verify(singleMock).add("was added first");
        inOrder.verify(singleMock).add("was added second");
        // B. Multiple mocks that must be used in a particular order
        List firstMock = mock(List.class);
        List secondMock = mock(List.class);
        //using mocks
        firstMock.add("was called first");
        secondMock.add("was called second");
        //create inOrder object passing any mocks that need to be verified in order
        InOrder inOrder2 = inOrder(firstMock, secondMock);
        //following will make sure that firstMock was called before secondMock
        inOrder2.verify(firstMock).add("was called first");
        inOrder2.verify(secondMock).add("was called second");
        // Oh, and A + B can be mixed together at will
为连续的调用做测试桩 (stub)
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.第一次调用返回 "angst",第二次返回"amy",第三次返回"Exception"
        when(person.getName()).thenReturn("angst").thenReturn("amy").thenThrow(new RuntimeException("第三次调用会返回异常"));
        System.out.println("第一次调用:" + person.getName());
        System.out.println("第二次调用:" + person.getName());
        System.out.println("第三次调用:" + person.getName());
        // 3.另外,连续调用的另一种更简短的版本
        when(person.getName()).thenReturn("angst", "amy").thenThrow(new RuntimeException("第三次调用会返回异常"));
为回调做测试桩
        // 1.Mock Person Object
        Person person = mock(Person.class);
        // 2.运行为泛型接口 Answer 打桩
        when(person.getName()).thenAnswer(new Answer() {
            public Object answer(InvocationOnMock invocation) {
                Object[] args = invocation.getArguments();
                Object mock = invocation.getMock();
                return "called with arguments: " + Arrays.toString(args) + " object: "+ mock;
            }
        });
        //Following prints "called with arguments: [] object: Mock for Person, hashCode: 426394307"
        System.out.println(person.getName());
简化mock对象的创建
    @Mock
    private Person person;
    @Mock
    private UserSearchServiceImpl userSearchService;

    //注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中
    @BeforeClass
    public void setUp(){ MockitoAnnotations.initMocks(this); }

三、模拟真实接口实战

1.正常模拟场景

首先来 mock 一个正常接口方法的返回值。假设需要测试一个 UserSearchServiceImpl#getInfo 接口方法,需要对接口返回修改指定的返回值,然后调用该接口,最后断言接口返回。下面看案例:PersonTest.java

package org.example.angst;

import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static org.mockito.Mockito.*;

public class PersonTest {
    @Mock
    private UserSearchServiceImpl userSearchService;
    private final Person actualResult = new Person("amy", 18, "girls");

    @BeforeClass
    public void before() {
        MockitoAnnotations.initMocks(this);
    }

    /**
     * 首先定义Person类
     * 定义一个UserSearchService接口,定义个getInfo()方法
     * UserSearchServiceImpl实现该接口,重写getInfo()方法,返回一个Person对象
     */
    @Test(description = "正常mock实现类接口")
    public void testNormalAnalogCall (){
        //1.模拟 getInfo() 方法返回值
        when(userSearchService.getInfo()).thenReturn(new Person("amy", 18, "girls"));
        //2.调用接口
        Person response = userSearchService.getInfo();
        //3.断言接口返回和actualResult是否相等
        Assert.assertEquals(response.getName(), actualResult.getName());
    }
}

2.回调接口场景

本文的重点来了!有些时候需要测试有回调接口函数,一般来说它们是异步执行的。很显然测试起来并不那么轻松,如果使用Thread.sleep(milliseconds)来等待它们执行完成只能说是一种比较low的实现,并且会让你的测试具有不确定性。这时候就体现 Mockito 的强大之处.

例:假设我们有一个实现了 DummyCallback 接口的 DummyCallbackImpl,在 DummyCallbackImpl 中有一个doSomethingAsynchronously()方法,该方法会调用构造方法中传入的 DummyCollaborator对象,并调用其 doSomethingAsynchronously(DummyCallback callback),而它的任务在后台线程中执行完成之后就会回调这个callback 对象的 onSuccess() 方法。

下面直接看示例:
DummyCallback.java

package org.example.angst;

import java.util.List;

/**
* 提供了两个抽象方法
* void onSuccess(List<String> result);
* void onFail(int code);
*/
public interface DummyCallback {
    void onSuccess(List<String> result);
    void onFail(int code);
}

DummyCallbackImpl.java

package org.example.angst;

import java.util.ArrayList;
import java.util.List;

/**
* 实现了 DummyCallback 接口,构造方法入参为 DummyCollaborator 对象
* 提供了doSomethingAsynchronously() 方法默认传入this
*/
public class DummyCallbackImpl implements DummyCallback {

    private final DummyCollaborator dummyCollaborator;
    private  List<String> result = new ArrayList<>();

    public DummyCallbackImpl(DummyCollaborator dummyCollaborator) {
        this.dummyCollaborator = dummyCollaborator;
    }

    public void doSomethingAsynchronously() {
        dummyCollaborator.doSomethingAsynchronously(this);
    }

    public List<String> getResult() {
        return this.result;
    }

    @Override
    public void onSuccess(List<String> result) {
        this.result = result;
        System.out.println("On success");

    }

    @Override
    public void onFail(int code) {
        System.out.println("On Fail"+ code);
    }
}

DummyCollaborator.java

package org.example.angst;

import static java.util.Collections.EMPTY_LIST;

/**
* 异步执行操作类,定义 `doSomethingAsynchronously()` 方法,
* 开启线程回调 `DummyCallback` 对象的两个方法
*/
public class DummyCollaborator {
    public static int ERROR_CODE = 1;

    public void doSomethingAsynchronously (final DummyCallback callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    callback.onSuccess(EMPTY_LIST);
                } catch (InterruptedException e) {
                    callback.onFail(ERROR_CODE);
                    e.printStackTrace();
                }
            }
        }).start();
    }
}
定义测试类

下面会提供2种不同的方法来测试定义好的回调接口,但是首先我们先创建一个DummyCollaboratorCallerTest 测试类。

package org.example.angst;

import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import java.util.Arrays;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;


public class DummyCollaboratorCallerTest {
    @Mock
    private DummyCallbackImpl dummyCallbackImpl;
    @Mock
    private DummyCollaborator mockDummyCollaborator;
    @Captor
    private ArgumentCaptor<DummyCallback> dummyCallbackArgumentCaptor;

    @BeforeClass
    public void before() {
        MockitoAnnotations.initMocks(this);
        this.dummyCallbackImpl = new DummyCallbackImpl(mockDummyCollaborator);
    }

    @Test
    public void testDoSomethingAsynchronouslyUsingDoAnswer() {}

    @Test
    public void testDoSomethingAsynchronouslyUsingArgumentCaptor() {}
}
doAnswer 测试回调接口
    /**
     * 这是我们使用doAnswer()来为一个函数进行打桩以测试异步函数的测试用例。这意味着我们需要理解返回一个回调(同步的),
     * 当被测试的方法被调用时我们生成了一个通用的 answer,这个回调会被执行。
     * 最后,我们调用了doSomethingAsynchronously函数,并且验证了状态和交互结果。
     */
    @Test(description = "doAnswer 测试回调接口")
    public void testDoSomethingAsynchronouslyUsingDoAnswer() {
        // 1.为callback执行一个同步 answer
        final List<String> results = Arrays.asList("One", "Two", "Three");
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) {
                ((DummyCallback)invocation.getArguments()[0]).onSuccess(results);
                return null;
            }
        }).when(mockDummyCollaborator).doSomethingAsynchronously(any(DummyCallback.class));
        // 2.调用被测试的函数
        dummyCallbackImpl.doSomethingAsynchronously();
        // 3.验证状态与结果
        verify(mockDummyCollaborator, times(1)).doSomethingAsynchronously(any(DummyCallback.class));
        Assert.assertEquals(dummyCallbackImpl.getResult(), results);
    }
ArgumentCaptor 测试异步回调接口
    /**
     *第二种实现是使用ArgumentCaptor。在这里我们的callback是异步的: 我们通过ArgumentCaptor捕获传递到DummyCollaborator对象的DummyCallback回调
     * 最终,我们可以在测试函数级别进行所有验证,当我们想验证状态和交互结果时可以调用 onSuccess()
     */
    @Test(description = "ArgumentCaptor 测试异步回调接口")
    public void testDoSomethingAsynchronouslyUsingArgumentCaptor() {
        final List<String> results = Arrays.asList("One", "Two", "Three");
        // 1.调用要被测试发函数
        dummyCallbackImpl.doSomethingAsynchronously();
        // 2.Let's call the callback. ArgumentCaptor.capture() works like a matcher.
        verify(mockDummyCollaborator, times(1)).doSomethingAsynchronously(
                dummyCallbackArgumentCaptor.capture());
        // 3.在执行回调之前验证结果
        Assert.assertTrue(dummyCallbackImpl.getResult().isEmpty());
        // 4.调用回调的onSuccess函数
        dummyCallbackArgumentCaptor.getValue().onSuccess(results);
        // 5.再次验证结果
        Assert.assertEquals(dummyCallbackImpl.getResult(), results);
    }

Epilogue

以上两种实现的主要的不同点是在当使用 DoAnswer() 方案时我们创建了一个匿名内部类,并且将它的元素从invocation.getArguments()[n]转换到我们需要的类型,当万一这个类型匹配失败,那么对应用例也会失败。另一方面,当我们使用 ArgumentCaptor 时我们可能能够更精准的控制测试用例,因为我们能够捕获mock对象,并且能够通过手动来操作回调对象。以上两种方式虽然都能实现回调接口的mock,但是我更倾向于第二种。


上一篇下一篇

猜你喜欢

热点阅读