一、前言
之前接觸到一次SaaS項(xiàng)目,在進(jìn)行Dubbo接口測(cè)試時(shí),由于某些API是回調(diào)接口通過(guò)MQ交互傳遞消息的,走的異步處理。因?yàn)镸Q在消費(fèi)消息的時(shí)候可能存在延遲,在自動(dòng)化用例調(diào)用接口步驟之后,立即查詢(xún)數(shù)據(jù)庫(kù)結(jié)果可能會(huì)存在消息還在消費(fèi)中數(shù)據(jù)并沒(méi)有落庫(kù),導(dǎo)致斷言case會(huì)失敗,而接口本身定義返回的 message 對(duì)于assert沒(méi)有實(shí)際的意義,這就造成了在assert時(shí)候的麻煩。因?yàn)槟悴恢繫Q消息何時(shí)才消費(fèi)完成并落庫(kù),導(dǎo)致用例的不確定性影響通過(guò)率。剛開(kāi)始的思路是通過(guò)多次遍歷查詢(xún)數(shù)據(jù)庫(kù),但是結(jié)果并不是很理想,因?yàn)楸闅v等待的時(shí)間會(huì)嚴(yán)重影響用例執(zhí)行的效率,通過(guò)率也不符合預(yù)期。
針對(duì)以上問(wèn)題,經(jīng)過(guò)考慮是否能夠采用mock的方式來(lái)解決。上網(wǎng)查詢(xún)相關(guān)資料,發(fā)現(xiàn) Mockito+PowerMockito 框架,提供的相關(guān)API很好的解決了以上問(wèn)題,不得不說(shuō) Mockito 是真的好用,強(qiáng)烈推薦真香!
二、Mockito 入門(mén)
POM依賴(lài)
<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>
模擬對(duì)象
// 1.Mock Person Object
Person person = mock(Person.class);
// 2.此時(shí)調(diào)用getName方法,會(huì)返回null,因?yàn)檫€沒(méi)有對(duì)方法調(diào)用的返回值做Mock
System.out.println(person.getName());
模擬對(duì)象返回值
// 1.Mock Person Object
Person person = mock(Person.class);
// 2.Mock 調(diào)用person對(duì)象的getName()方法時(shí),返回angst,在給特定的方法調(diào)用返回固定值在官方說(shuō)法中稱(chēng)為 stub 測(cè)試樁
when(person.getName()).thenReturn("angst");
// 3.此時(shí)打印輸出 angst
System.out.println(person.getName());
模擬指定異常
// 1.Mock Person Object
Person person = mock(Person.class);
// 2.模擬調(diào)用getAge()方法時(shí),拋出 RuntimeException
when(person.getAge()).thenThrow(new RuntimeException());
// 3.此時(shí)將會(huì)拋出 RuntimeException
System.out.println(person.getAge());
為返回值為void的函數(shù)通過(guò)Stub拋出異常
// 1.Mock Person Object
Person person = mock(Person.class);
// 2.用于為無(wú)返回值的函數(shù)打樁
doThrow(new RuntimeException("Custom Exception")).when(person).setAge(18);
// 3.調(diào)用這句代碼會(huì)拋出異常
person.setAge(18);
參數(shù)匹配器 (matchers)
// 1.Mock Person Object
LinkedList mockedList = mock(LinkedList.class);
// 使用內(nèi)置的anyInt()參數(shù)匹配器,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());
驗(yàn)證調(diào)用次數(shù)
// 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.下面兩個(gè)寫(xiě)法驗(yàn)證效果一樣,均驗(yàn)證 setName() 方法是否被調(diào)用了一次,verify 函數(shù)默認(rèn)驗(yàn)證的是執(zhí)行了times(1)
也就是某個(gè)測(cè)試函數(shù)是否執(zhí)行了1次.因此,times(1)通常被省略了,如果 times=2 則拋出異常 TooLittleActualInvocations*/
verify(person).setName("amy");
verify(person, times(1)).setName("amy");
// 3.使用never()進(jìn)行驗(yàn)證,never相當(dāng)于times(0)
verify(person, never()).setAge(18);
//atLeastOnce相當(dāng)于times(1)
verify(person, atLeastOnce()).setName("amy");
//atLeast至少調(diào)用N次
verify(person, atLeast(2)).setName("angst");
//atLeast最多調(diào)用N次
verify(person, atMost(3)).setName("thin bamboo");
驗(yàn)證執(zhí)行執(zhí)行順序
// 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
為連續(xù)的調(diào)用做測(cè)試樁 (stub)
// 1.Mock Person Object
Person person = mock(Person.class);
// 2.第一次調(diào)用返回 "angst",第二次返回"amy",第三次返回"Exception"
when(person.getName()).thenReturn("angst").thenReturn("amy").thenThrow(new RuntimeException("第三次調(diào)用會(huì)返回異常"));
System.out.println("第一次調(diào)用:" + person.getName());
System.out.println("第二次調(diào)用:" + person.getName());
System.out.println("第三次調(diào)用:" + person.getName());
// 3.另外,連續(xù)調(diào)用的另一種更簡(jiǎn)短的版本
when(person.getName()).thenReturn("angst", "amy").thenThrow(new RuntimeException("第三次調(diào)用會(huì)返回異常"));
為回調(diào)做測(cè)試樁
// 1.Mock Person Object
Person person = mock(Person.class);
// 2.運(yùn)行為泛型接口 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());
簡(jiǎn)化mock對(duì)象的創(chuàng)建
@Mock
private Person person;
@Mock
private UserSearchServiceImpl userSearchService;
//注意!下面這句代碼需要在運(yùn)行測(cè)試函數(shù)之前被調(diào)用,一般放到測(cè)試類(lèi)的基類(lèi)或者test runner中
@BeforeClass
public void setUp(){ MockitoAnnotations.initMocks(this); }
三、模擬真實(shí)接口實(shí)戰(zhàn)
1.正常模擬場(chǎng)景
首先來(lái) mock 一個(gè)正常接口方法的返回值。假設(shè)需要測(cè)試一個(gè) UserSearchServiceImpl#getInfo 接口方法,需要對(duì)接口返回修改指定的返回值,然后調(diào)用該接口,最后斷言接口返回。下面看案例: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類(lèi)
* 定義一個(gè)UserSearchService接口,定義個(gè)getInfo()方法
* UserSearchServiceImpl實(shí)現(xiàn)該接口,重寫(xiě)getInfo()方法,返回一個(gè)Person對(duì)象
*/
@Test(description = "正常mock實(shí)現(xiàn)類(lèi)接口")
public void testNormalAnalogCall (){
//1.模擬 getInfo() 方法返回值
when(userSearchService.getInfo()).thenReturn(new Person("amy", 18, "girls"));
//2.調(diào)用接口
Person response = userSearchService.getInfo();
//3.斷言接口返回和actualResult是否相等
Assert.assertEquals(response.getName(), actualResult.getName());
}
}
2.回調(diào)接口場(chǎng)景
本文的重點(diǎn)來(lái)了!有些時(shí)候需要測(cè)試有回調(diào)接口函數(shù),一般來(lái)說(shuō)它們是異步執(zhí)行的。很顯然測(cè)試起來(lái)并不那么輕松,如果使用Thread.sleep(milliseconds)來(lái)等待它們執(zhí)行完成只能說(shuō)是一種比較low的實(shí)現(xiàn),并且會(huì)讓你的測(cè)試具有不確定性。這時(shí)候就體現(xiàn) Mockito 的強(qiáng)大之處.
例:假設(shè)我們有一個(gè)實(shí)現(xiàn)了 DummyCallback 接口的 DummyCallbackImpl,在 DummyCallbackImpl 中有一個(gè)doSomethingAsynchronously()方法,該方法會(huì)調(diào)用構(gòu)造方法中傳入的 DummyCollaborator對(duì)象,并調(diào)用其 doSomethingAsynchronously(DummyCallback callback),而它的任務(wù)在后臺(tái)線(xiàn)程中執(zhí)行完成之后就會(huì)回調(diào)這個(gè)callback 對(duì)象的 onSuccess() 方法。
下面直接看示例:
DummyCallback.java
package org.example.angst;
import java.util.List;
/**
* 提供了兩個(gè)抽象方法
* 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;
/**
* 實(shí)現(xiàn)了 DummyCallback 接口,構(gòu)造方法入?yún)?DummyCollaborator 對(duì)象
* 提供了doSomethingAsynchronously() 方法默認(rèn)傳入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;
/**
* 異步執(zhí)行操作類(lèi),定義 `doSomethingAsynchronously()` 方法,
* 開(kāi)啟線(xiàn)程回調(diào) `DummyCallback` 對(duì)象的兩個(gè)方法
*/
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();
}
}
定義測(cè)試類(lèi)
下面會(huì)提供2種不同的方法來(lái)測(cè)試定義好的回調(diào)接口,但是首先我們先創(chuàng)建一個(gè)DummyCollaboratorCallerTest 測(cè)試類(lèi)。
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 測(cè)試回調(diào)接口
/**
* 這是我們使用doAnswer()來(lái)為一個(gè)函數(shù)進(jìn)行打樁以測(cè)試異步函數(shù)的測(cè)試用例。這意味著我們需要理解返回一個(gè)回調(diào)(同步的),
* 當(dāng)被測(cè)試的方法被調(diào)用時(shí)我們生成了一個(gè)通用的 answer,這個(gè)回調(diào)會(huì)被執(zhí)行。
* 最后,我們調(diào)用了doSomethingAsynchronously函數(shù),并且驗(yàn)證了狀態(tài)和交互結(jié)果。
*/
@Test(description = "doAnswer 測(cè)試回調(diào)接口")
public void testDoSomethingAsynchronouslyUsingDoAnswer() {
// 1.為callback執(zhí)行一個(gè)同步 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.調(diào)用被測(cè)試的函數(shù)
dummyCallbackImpl.doSomethingAsynchronously();
// 3.驗(yàn)證狀態(tài)與結(jié)果
verify(mockDummyCollaborator, times(1)).doSomethingAsynchronously(any(DummyCallback.class));
Assert.assertEquals(dummyCallbackImpl.getResult(), results);
}
ArgumentCaptor 測(cè)試異步回調(diào)接口
/**
*第二種實(shí)現(xiàn)是使用ArgumentCaptor。在這里我們的callback是異步的: 我們通過(guò)ArgumentCaptor捕獲傳遞到DummyCollaborator對(duì)象的DummyCallback回調(diào)
* 最終,我們可以在測(cè)試函數(shù)級(jí)別進(jìn)行所有驗(yàn)證,當(dāng)我們想驗(yàn)證狀態(tài)和交互結(jié)果時(shí)可以調(diào)用 onSuccess()
*/
@Test(description = "ArgumentCaptor 測(cè)試異步回調(diào)接口")
public void testDoSomethingAsynchronouslyUsingArgumentCaptor() {
final List<String> results = Arrays.asList("One", "Two", "Three");
// 1.調(diào)用要被測(cè)試發(fā)函數(shù)
dummyCallbackImpl.doSomethingAsynchronously();
// 2.Let's call the callback. ArgumentCaptor.capture() works like a matcher.
verify(mockDummyCollaborator, times(1)).doSomethingAsynchronously(
dummyCallbackArgumentCaptor.capture());
// 3.在執(zhí)行回調(diào)之前驗(yàn)證結(jié)果
Assert.assertTrue(dummyCallbackImpl.getResult().isEmpty());
// 4.調(diào)用回調(diào)的onSuccess函數(shù)
dummyCallbackArgumentCaptor.getValue().onSuccess(results);
// 5.再次驗(yàn)證結(jié)果
Assert.assertEquals(dummyCallbackImpl.getResult(), results);
}
Epilogue
以上兩種實(shí)現(xiàn)的主要的不同點(diǎn)是在當(dāng)使用 DoAnswer() 方案時(shí)我們創(chuàng)建了一個(gè)匿名內(nèi)部類(lèi),并且將它的元素從invocation.getArguments()[n]轉(zhuǎn)換到我們需要的類(lèi)型,當(dāng)萬(wàn)一這個(gè)類(lèi)型匹配失敗,那么對(duì)應(yīng)用例也會(huì)失敗。另一方面,當(dāng)我們使用 ArgumentCaptor 時(shí)我們可能能夠更精準(zhǔn)的控制測(cè)試用例,因?yàn)槲覀兡軌虿东@mock對(duì)象,并且能夠通過(guò)手動(dòng)來(lái)操作回調(diào)對(duì)象。以上兩種方式雖然都能實(shí)現(xiàn)回調(diào)接口的mock,但是我更傾向于第二種。