Mockito是一個模擬測試框架,可以讓你用優雅,簡潔的接口寫出漂亮的單元測試。Mockito可以讓單元測試易於可讀,產生簡潔的校驗錯誤。
1、如何使用Mockito
引入mavne依賴
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
有三種方法可以配置使用 Mockito
:
MockitoJUnitRunner 注解
接下來主要用這種方法來介紹
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.Mock;
// 測試類加上 @RunWith(MockitoJUnitRunner.class) 注解
@RunWith(MockitoJUnitRunner.class)
public class MockByRunnerTest {
@Mock
private AccountDao accountDao;
}
MockitoAnnotations 方式
import org.mockito.MockitoAnnotations;
import org.mockito.Mock;
import org.junit.Before;
public class MockByAnnotationTest {
@Mock
private AccountDao accountDao;
@Before
public void init(){
// 初始化
MockitoAnnotations.initMocks(this);
}
}
@Rule 注解
import org.junit.Rule;
import org.mockito.Mock;
public class MockByRuleTest {
// 初始化
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock
AccountDao accountDao;
}
2、什么是Mock測試
mock測試就是在測試過程中,對於某些不容易構造或者不容易獲取的對象,用一個虛擬的對象來創建以便測試的測試方法。
比如一個類A,有B、C、D等多個復雜類的成員變量。如果我們測試時候通過new A()
的方式來創建A對象,就需要同時手動創建B、C、D三個對象,並和A關聯,提高了測試的復雜性。Mock可以自動生成一個虛擬的A對象,就是幫我們省去了這些復雜的創建流程。
Mockito提供了兩種Mock的方式:
import org.mockito.Mock;
import static org.mockito.Mockito.mock;
@RunWith(MockitoJUnitRunner.class)
public class MockByRunnerTest2 {
// 使用@Mock注解
@Mock
private AccountDao accountDao;
@Test
public void test1() {
Account account = accountDao.findById(1);
// 返回null
System.out.println(account;
// 輸出 class com.sanyue.learn.mockito.mockitodemo.domain.Account$MockitoMock$1675715420
System.out.println(account.getClass());
}
public void test2() {
// 使用mock方法
AccountDao accountDao = mock(AccountDao.class, Mockito.RETURNS_SMART_NULLS);
Account account = accountDao.findById(1);
// 返回null
System.out.println(accoun;
}
}
Mock生成的是一個代理對象,默認情況下,執行對象的所有的方法都返回該方法的返回類型的默認值,不會真正去執行該對象的方法。既然這樣,那我們在測試中如何使用這個mock出來的對象,來執行方法進行測試呢?這就需要使用到Mockito的Stub(測試樁)來設置Mock對象方法的返回值了。
3、Stub(測試樁) 介紹
上面介紹了Mock生成的對象,其實是一個代理對象,不會真正去執行類里面的方法。為了便於我們測試,我們需要用到Stub來設置我們期望的方法返回值,可以理解為創建測試用例。
// 你可以mock具體的類型,不僅只是接口
LinkedList mockedList = mock(LinkedList.class);
// 開始設置測試樁
// 當get(0)被調用時,返回"first"
when(mockedList.get(0)).thenReturn("first");
// 方法get(1)被調用時,拋異常。
when(mockedList.get(1)).thenThrow(new RuntimeException());
// 輸出 "first"
System.out.println(mockedList.get(0));
// 拋出異常
System.out.println(mockedList.get(1));
// 輸出 null,因為get(999)的調用沒有被設置過
System.out.println(mockedList.get(999));
由上面例子可以看到,Stub就是人為指定 當使用該參數調用該方法時,方法返回什么值。下面介紹一些其他的Stud方式:
// 使用doReturn語句和when語句一樣的效果
doReturn(1).when(mockedList).get(1);
// 輸出 1
System.out.println(mockedList.get(1));
// 使用doNothing來設置void返回值的方法
doNothing().when(mockedList).clear();
// 設置執行clear方法拋出異常
doThrow(new RuntimeException()).when(mockedList).clear();
mockedList.clear();
// 以下斷言表示,mockedList的clear方法被調用了1次
verify(mockedList, times(1)).clear();
設置每次調用返回不同的值
如果希望每次調用的返回值都不一樣可以這樣設置:
// 第1次調用返回2,第2次返回2,以后再調用返回3
when(mockedList.size()).thenReturn(1, 2, 3);
// 等價寫法
// when(mockedList.size()).thenReturn(1).thenReturn(2).thenReturn(3).thenReturn(4);
// 1
System.out.println(mockedList.size());
// 2
System.out.println(mockedList.size());
// 3
System.out.println(mockedList.size());
// 超過3次后調用,也返回3
System.out.println(mockedList.size());
也可以通過thenAnswer
方式來設置不同調用次數返回不同的值:
// 設置返回值是 參數值*10
when(list.get(anyInt())).thenAnswer(new Answer(){
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
int arguments = invocationOnMock.getArgument(0);
return 10*arguments;
}
});
參數匹配器
設置用參數匹配器根據不同類型參數,返回不同的值:
public class TestService {
// 定義一個方法
public String say(String param1, Integer param2, String param3) {
return "hello";
}
}
@Test
public void test3(){
TestService testService = mock(TestService.class);
// anyString() 表示任何字符串參數,anyInt() 表示任何int類型參數
when(testService.say(anyString(), anyInt(), anyString())).thenReturn("world");
// 輸出 world
System.out.println(testService.say("x", 1, "x"));
// 如果參數列表包含參數匹配器,則必能出現具體參數值,要使用eq() 方法代替
// when(testService.say(anyString(), 1, anyString())).thenReturn("world2");
when(testService.say(anyString(), eq(1), anyString())).thenReturn("world2");
// 輸出 world2
System.out.println(testService.say("x", 1, "x"));
}
設置執行真實的方法
可以使用thenCallRealMethod
來設置執行對象真正的方法
List list = mock(LinkedList.class);
when(list.size()).thenCallRealMethod();
重置Mock對象
使用reset
方法可以重置Mock對象Stub的設置
List mock = mock(List.class);
when(mock.size()).thenReturn(10);
mock.add(1);
reset(mock);
do系列方法的運用
當你調用doThrow()
, doAnswer()
, doNothing()
, doReturn()
and doCallRealMethod()
這些函數時可以在適當的位置調用when()
函數. 當你需要下面這些功能時這是必須的:
- 測試void函數
- 在受監控的對象上測試函數
- 不知一次的測試為同一個函數,在測試過程中改變mock對象的行為,比如為
Spy
對象進行Stub
像anyObject()
, eq()
這樣的匹配器函數不會返回匹配器。它們會在內部將匹配器記錄到一個棧當中,並且返回一個假的值,通常為null。這樣的實現是由於被Java編譯器強加的靜態類型安全
。結果就是你不能在驗證或者測試樁函數之外使用anyObject()
, eq()
函數。
如果一個方法沒有被Stub設置,會返回該方法返回類型的默認值,比如int類型返回0,boolean返回false,對象類型返回null。
需要記住的是 mock對象會覆蓋整個被mock的對象,因此沒有stub的方法只能返回默認值,並且類的方法不會真正的執行。
4、Spy 介紹
Mock
出來的對象(代理對象),默認不會去真正執行類的方法。而用Spy
聲明的對象(真實對象),則會默認執行真正的方法。
/**
* 也可以使用@Spy注解方式初始化spy對象
* @Spy
* List<Integer> list = new ArrayList<>();
**/
List<Integer> realList = new ArrayList<>();
List<Integer> list = spy(realList);
list.add(1);
list.add(2);
// 分別輸出1和2,說明真正執行了add和get方法
System.out.println(list.get(0));
System.out.println(list.get(1));
// 進行部分mock
when(list.isEmpty()).thenReturn(true);
// 輸出true,說明isEmpty方法被mock了
System.out.println(list.isEmpty());
// 分別輸出1和2,說明get方法不受mock影響
System.out.println(list.get(0));
System.out.println(list.get(1));
需要注意的是,如果為Spy
出來的對象進行Stub
,有時候不能使用when
,因為Spy
對象調用方法時,會調用真實的方法。比如以下例子:
List list = new LinkedList();
List spy = spy(list);
// 不可能 : 因為當調用spy.get(0)時會調用真實對象的get(0)函數,此時會發生IndexOutOfBoundsException異常,因為真實List對象是空的
when(spy.get(0)).thenReturn("foo");
System.out.println(spy.get(0));
// 你需要使用doReturn()來打樁
doReturn("foo").when(spy).get(0);
System.out.println(spy.get(0));
Spy
和Mock
的相同點和區別:
-
得到的對象同樣可以進行“監管”,即驗證和打樁。
-
如果不對spy對象的methodA打樁,那么調用spy對象的methodA時,會調用真實方法。
-
如果不對mock對象的methodA打樁,將doNothing,且返回默認值(null,0,false)。
5、斷言
Mockito
中斷言的使用和Junit
的一樣,這里舉幾個例子,不詳細描述:
List<Integer> list = mock(List.class);
// 斷言list.get(0)值等於1
assertThat(list.get(0), equalTo(1));
// 斷言大於50
assertThat(list.get(0), greaterThan(20));
// 斷言小於等於50
assertThat(list.get(0), lessThanOrEqualTo(50));
// 斷言 必須大於20 並且 小於等於50(所有條件成立)
assertThat(list.get(0), allOf(greaterThan(20), lessThanOrEqualTo(50)));
// 斷言 必須大於20 或 小於等於50(其中至少一個條件成立)
assertThat(list.get(0), oneOf(greaterThan(20), lessThanOrEqualTo(50)));
// 斷言任何條件都成立
assertThat(list.get(0), anything());
// 斷言等於1
assertThat(list.get(0), is(1));
// 斷言不等於-1
assertThat(list.get(0), not(-1));
// 斷言返回的字符串包含1
assertThat(list.get(0), containsString("1"));
// 斷言返回的字符串以1開頭
assertThat(list.get(0), startsWith("1"));
// 斷言該異常屬於RuntimeException
assertThat(e, instanceOf(RuntimeException.class));
可以這樣斷言異常
try {
list.clear();
// 如果執行到這一步,返回失敗
fail();
} catch (Exception e) {
assertThat(e, instanceOf(RuntimeException.class));
}
6、驗證函數的調用次數
Mockito
可以對函數的執行過程進行斷言,通過斷言函數的執行次數,要對方法執行邏輯進行判斷。
List<Integer> mockedList = mock(List.class);
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
// 下面的兩個驗證函數效果一樣,期望mockedList的add("once")方法執行了1次
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
// 驗證具體的執行次數,分別希望是2次和3次
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
// 使用never()進行驗證,never相當於times(0),即沒有執行過
verify(mockedList, never()).add("never happened");
// 使用atLeast()至少執行次數/atMost()最多執行次數
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");
7、驗證方法的執行順序
可以使用InOrder
來對方法的執行順序進行驗證
// 進行mock
List singleMock = mock(List.class);
singleMock.add("was added first");
singleMock.add("was added second");
// 為該mock對象創建一個inOrder對象
InOrder inOrder = inOrder(singleMock);
// 確保add函數首先執行的是add("was added first"),然后才是add("was added second")
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");