一、前言
之前接触到一次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,但是我更倾向于第二种。
网友评论