Android單元測試之Mockito

背景

在寫單元測試的過程中,一個很普遍的問題是,要測試的目標類會有很多依賴,這些依賴的類/對象/資源又會有別的依賴,從而形成一個大的依賴樹,要在單元測試的環境中完整地構建這樣的依賴,是一件很困難的事情。

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有兩個重載方法:

  1. verify(T mock): 默認是驗證調用一次,里面默認調用times(1)
  2. 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);
}
  1. 對于有返回值的方法,mock會默認返回null、空集合、默認值。比如,為int/Integer返回0,為boolean/Boolean返回false
  2. stubbing可以被覆蓋,但是請注意覆蓋已有的stubbing有可能不是很好
  3. 一旦stubbing,不管調用多少次,方法都會永遠返回stubbing的值
  4. 當你對同一個方法進行多次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對象有如下的優點:

  1. 方便mock對象的創建
  2. 減少mock對象創建的重復代碼
  3. 提高測試代碼可讀性
  4. 變量名字作為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());

    }

}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容