背景
在寫單元測試的過程中,一個很普遍的問題是,要測試的目標類會有很多依賴,這些依賴的類/對象/資源又會有別的依賴,從而形成一個大的依賴樹,要在單元測試的環境中完整地構建這樣的依賴,是一件很困難的事情。
Mock就是解決的方案。簡單地說就是對測試的類所依賴的其他類和對象,進行mock - 構建它們的一個假的對象,定義這些假對象上的行為,然后提供給被測試對象使用。被測試對象像使用真的對象一樣使用它們。用這種方式,我們可以把測試的目標限定于被測試對象本身,就如同在被測試對象周圍做了一個劃斷,形成了一個盡量小的被測試目標。
Mockito是什么
Mockito是一套非常強大的測試框架,被廣泛的應用于Java程序的unit test中。相比于EasyMock框架,Mockito使用起來簡單,學習成本很低,而且具有非常簡潔的API,測試代碼的可讀性很高。
Mockito使用
配置依賴:
testCompile "org.mockito:mockito-core:1.10.19"
先來看看Mockito的基礎使用。比如我們有以下幾個類:
public class Person {
private int id;
private String name;
public Person(int id,String name){
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public interface PersonDAO {
Person getPerson(int id);
boolean update(Person person);
}
public class PersonService {
private final PersonDAO personDAO;
public PersonService(PersonDAO personDAO){
this.personDAO = personDAO;
}
public boolean update(int id, String name) {
Person person = personDAO.getPerson(id);
if (person == null) {
return false;
}
Person personUpdate = new Person(person.getId(), name);
return personDAO.update(personUpdate);
}
}
說明: 以上是開發中基礎的mvc分層結構,比如在開發中,PersonDAO的具體實現還未完成,這時候就可以通過mock來mock一個實例來做測試。
來看一下測試時怎么寫的,這里我們主要對PersonService 中的update方法寫測試用例。
public class PersonServiceTest {
private PersonDAO mockDao;
private PersonService personService;
@Before
public void setUp() throws Exception {
//模擬PersonDao對象
mockDao = Mockito.mock(PersonDAO.class);
Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,"Jim"));
Mockito.when(mockDao.update(Mockito.isA(Person.class))).thenReturn(true);
personService = new PersonService(mockDao);
}
@Test
public void testUpdate() throws Exception {
boolean result = personService.update(1,"Tom");
assertTrue("is true",result);
//驗證是否執行過一次getPerson(1)
Mockito.verify(mockDao,Mockito.times(1)).getPerson(Mockito.eq(1));
//驗證是否執行過一次update
Mockito.verify(mockDao,Mockito.times(1)).update(Mockito.isA(Person.class));
}
@Test
public void testUpdateNotFind() throws Exception {
boolean result = personService.update(2, "new name");
assertFalse("must true", result);
//驗證是否執行過一次getPerson(2)
Mockito.verify(mockDao, Mockito.times(1)).getPerson(Mockito.eq(2));
//驗證是否執行過一次update
Mockito.verify(mockDao, Mockito.never()).update(Mockito.isA(Person.class));
}
}
簡單說明一下:
- 首先在setUp中,我們先模擬一個對象出來,主要通過Mockito.mock(PersonDAO.class);來mock的。
- 然后添加Stubbind條件,Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,"Jim")); 意思是當調用mockDao.getPerson(1)時返回一個id為1,name為"Jim"的Person對象。
- 在testUpdate()方法中 Mockito.verify(mockDao,Mockito.times(1)).getPerson(Mockito.eq(1));驗證是否執行過一次getPerson(1)。只要有執行過Mockito都會記錄下拉,所以這句是對的。
Mockito基礎使用
Mockito的使用,有詳細的api文檔,具體可以查看:http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html, 下面是整理的一些常用的使用方式。
verify 驗證
一旦創建,mock會記錄所有交互,你可以驗證所有你想要驗證的東西,即使刪掉也會有操作記錄在。
@Test
public void testVerify() throws Exception {
//mock creation
List mockList = Mockito.mock(List.class);
mockList.add("one");
mockList.add("two");
mockList.add("two");
mockList.clear();
//驗證是否調用過一次 mockedList.add("one")方法,若不是(0次或者大于一次),測試將不通過,默認是一次
Mockito.verify(mockList).add("one");
//驗證調用過2次 mockedList.add("two")方法,若不是,測試將不通過
Mockito.verify(mockList,Mockito.times(2)).add("two");
//驗證是否調用過一次 mockedList.clear()方法,若沒有(0次或者大于一次),測試將不通過
Mockito.verify(mockList).clear();
}
這里主要注意。mock會記錄你所有的操作的,即使刪除也會記錄下來。比如mockList中添加完,然后clear掉,Mockito.verify(mockList).add("one");這個的驗證也是會通過的,驗證的關鍵方法是verify, verify有兩個重載方法:
- verify(T mock): 默認是驗證調用一次,里面默認調用times(1)。
- verify(T mock, VerificationMode mode):mode,調用次數.
Stubbing 條件
@Test
public void testStubbing() throws Exception{
//你可以mock具體的類,而不僅僅是接口
LinkedList mockedList = Mockito.mock(LinkedList.class);
//設置值
Mockito.when(mockedList.get(0)).thenReturn("one");
Mockito.when(mockedList.get(1)).thenReturn("two");
Mockito.when(mockedList.get(2)).thenReturn(new RuntimeException());
//print 輸出"one"
System.out.println(mockedList.get(0));
//輸出 "java.lang.RuntimeException"
System.out.println(mockedList.get(2));
//這里會打印 "null" 因為 get(999) 沒有設置
System.out.println(mockedList.get(999));
Mockito.verify(mockedList).get(0);
}
- 對于有返回值的方法,mock會默認返回null、空集合、默認值。比如,為int/Integer返回0,為boolean/Boolean返回false
- stubbing可以被覆蓋,但是請注意覆蓋已有的stubbing有可能不是很好
- 一旦stubbing,不管調用多少次,方法都會永遠返回stubbing的值
- 當你對同一個方法進行多次stubbing,最后一次stubbing是最重要的
ArgumentMatcher參數匹配
@Test
public void testArgumentMatcher() throws Exception {
LinkedList mockedList = Mockito.mock(LinkedList.class);
//用內置的參數匹配器來stub
Mockito.when(mockedList.get(Mockito.anyInt())).thenReturn("element");
//打印 "element"
System.out.println(mockedList.get(999));
//你也可以用參數匹配器來驗證,此處測試通過
Mockito.verify(mockedList).get(Mockito.anyInt());
//此處測試將不通過,因為沒調用get(33)
Mockito.verify(mockedList).get(Mockito.eq(33));
}
InvocationTimes驗證準確的調用次數
驗證準確的調用次數包括最多、最少、從未等,times(),never(),atLeast(),atMost().
/**
* 驗證準確的調用次數,最多、最少、從未等
* @throws Exception
*/
@Test
public void testInvocationTimes() throws Exception {
LinkedList mockedList = Mockito.mock(LinkedList.class);
//using mock
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//下面兩個是等價的, 默認使用times(1)
Mockito.verify(mockedList).add("once");
Mockito.verify(mockedList, Mockito.times(1)).add("once");
//驗證準確的調用次數
Mockito.verify(mockedList, Mockito.times(2)).add("twice");
Mockito.verify(mockedList, Mockito.times(3)).add("three times");
//從未調用過. never()是times(0)的別名
Mockito.verify(mockedList, Mockito.never()).add("never happened");
//用atLeast()/atMost()驗證
Mockito.verify(mockedList, Mockito.atLeastOnce()).add("three times");
Mockito.verify(mockedList, Mockito.atLeast(2)).add("three times");
//最多
Mockito.verify(mockedList, Mockito.atMost(3)).add("three times");
}
為void方法拋異常
@Test
public void testVoidMethodsWithExceptions() throws Exception {
LinkedList mockedList = Mockito.mock(LinkedList.class);
Mockito.doThrow(new RuntimeException()).when(mockedList).clear();
//這邊會拋出異常
mockedList.clear();
}
InOrder驗證調用順序
@Test
public void testVerificationInOrder() throws Exception {
List singleMock = Mockito.mock(List.class);
//使用單個mock對象
singleMock.add("was added first");
singleMock.add("was added second");
//創建inOrder
InOrder inOrder = Mockito.inOrder(singleMock);
//驗證調用次數,若是調換兩句,將會出錯,因為singleMock.add("was added first")是先調用的
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
// 多個mock對象
List firstMock = Mockito.mock(List.class);
List secondMock = Mockito.mock(List.class);
//using mocks
firstMock.add("was called first");
secondMock.add("was called second");
//創建多個mock對象的inOrder
inOrder = Mockito.inOrder(firstMock, secondMock);
//驗證firstMock先于secondMock調用
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
}
spy
spy是創建一個拷貝,如果你保留原始的list,并用它來進行操作,那么spy并不能檢測到其交互
@Test
public void testSpy() throws Exception {
List list = new LinkedList();
List spy = Mockito.spy(list);
//可選的,你可以stub某些方法
Mockito.when(spy.size()).thenReturn(100);
//如果操作原始list,那么spy是不會檢測到的。
list.add("first");
//調用"真正"的方法
spy.add("one");
spy.add("two");
//打印one
System.out.println(spy.get(0));
//size()方法被stub了,打印100
System.out.println(spy.size());
//可選,驗證spy對象的行為
Mockito.verify(spy).add("one");
Mockito.verify(spy).add("two");
//下面寫法有問題,spy.get(10)會拋IndexOutOfBoundsException異常
Mockito.when(spy.get(10)).thenReturn("foo");
//可用以下方式
Mockito.doReturn("foo").when(spy).get(10);
}
Captur 參數捕捉
@Test
public void testCapturingArguments() throws Exception {
List mockedList = Mockito.mock(List.class);
ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
mockedList.add("John");
//進行參數捕捉,這里參數應該是"John"
Mockito.verify(mockedList).add(argument.capture());
assertEquals("John",argument.getValue());
}
Mock 的 Annotation,
Mockito跟junit4一樣也支持Annotation,Mockito支持的注解有:@Mock,@Spy(監視真實的對象),@Captor(參數捕獲器),@InjectMocks(mock對象自動注入)。
Annotation的初始化
在使用Annotation注解之前,必須先初始化,一般初始化在Junit4的@Before里面,初始化的方法為:MockitoAnnotations.initMocks(testClass)參數testClass是你所寫的測試類。
@Before
public void setUp() throws Exception {
/**
* 要想讓Annotation起作用,就必須初始化.一般初始化都在@Before里面
*/
MockitoAnnotations.initMocks(this);
}
@Mock注解
使用@Mock注解來定義mock對象有如下的優點:
- 方便mock對象的創建
- 減少mock對象創建的重復代碼
- 提高測試代碼可讀性
- 變量名字作為mock對象的標示,所以易于排錯
我們還是通過第一個例子來修改:
public class MockTest {
@Mock
private PersonDAO mockDao;
private PersonService personService;
@Before
public void setUp() throws Exception {
/**
* 要想讓Annotation起作用,就必須初始化.一般初始化都在@Before里面
*/
MockitoAnnotations.initMocks(this);
Mockito.when(mockDao.getPerson(1)).thenReturn(new Person(1,"Jim"));
Mockito.when(mockDao.update(Mockito.isA(Person.class))).thenReturn(true);
personService = new PersonService(mockDao);
}
@Test
public void testUpdate() throws Exception {
boolean result = personService.update(1,"Tom");
assertTrue("is true",result);
//驗證是否執行過一次getPerson(1)
Mockito.verify(mockDao,Mockito.times(1)).getPerson(Mockito.eq(1));
//驗證是否執行過一次update
Mockito.verify(mockDao,Mockito.times(1)).update(Mockito.isA(Person.class));
}
}
結果和前面沒用注解的一樣。
@Spy注解
使用@Spy生成的類,所有方法都是真實方法,返回值和真實方法一樣的,是使用Mockito.spy()的快捷方式.
public class MockTest {
@Spy
private List list = new LinkedList();
@Before
public void setUp() throws Exception {
/**
* 要想讓Annotation起作用,就必須初始化.一般初始化都在@Before里面
*/
MockitoAnnotations.initMocks(this);
}
@Test
public void testSpy() throws Exception {
//可選的,你可以stub某些方法
Mockito.when(list.size()).thenReturn(100);
//調用"真正"的方法
list.add("one");
list.add("two");
//打印one
System.out.println(list.get(0));
//size()方法被stub了,打印100
System.out.println(list.size());
}
}
@Captor注解
@Captor是參數捕獲器的注解,通過注解的方式可以更便捷的對ArgumentCaptor進行定義。還可以通過ArgumentCaptor對象的forClass(Class<T> clazz)方法來構建ArgumentCaptor對象,然后便可在驗證時對方法的參數進行捕獲,最后驗證捕獲的參數值。如果方法有多個參數都要捕獲驗證,那就需要創建多個ArgumentCaptor對象處理。
public class MockTest {
@Captor
private ArgumentCaptor<String> captor;
@Before
public void setUp() throws Exception {
/**
* 要想讓Annotation起作用,就必須初始化.一般初始化都在@Before里面
*/
MockitoAnnotations.initMocks(this);
}
@Test
public void testCaptor() throws Exception {
/**
* ArgumentCaptor的Api
argument.capture() 捕獲方法參數;
argument.getValue() 獲取方法參數值,如果方法進行了多次調用,它將返回最后一個參數值;
argument.getAllValues() 方法進行多次調用后,返回多個參數值;
*/
list.add("John");
//進行參數捕捉,這里參數應該是"John"
Mockito.verify(list).add(captor.capture());
assertEquals("John",captor.getValue());
}
}