Mockito使用指南

轉載請標明出處:http://blog.csdn.net/shensky711/article/details/52771493
本文出自: 【HansChen的博客】


mock和Mockito的關係

在軟件開發中提及”mock”,通常理解爲模擬對象。

爲什麼需要模擬? 在我們一開始學編程時,我們所寫的對象通常都是獨立的,並不依賴其他的類,也不會操作別的類。但實際上,軟件中是充滿依賴關係的,比如我們會基於service類寫操作類,而service類又是基於數據訪問類(DAO)的,依次下去,形成複雜的依賴關係。

單元測試的思路就是我們想在不涉及依賴關係的情況下測試代碼。這種測試可以讓你無視代碼的依賴關係去測試代碼的有效性。核心思想就是如果代碼按設計正常工作,並且依賴關係也正常,那麼他們應該會同時工作正常。

有些時候,我們代碼所需要的依賴可能尚未開發完成,甚至還不存在,那如何讓我們的開發進行下去呢?使用mock可以讓開發進行下去,mock技術的目的和作用就是模擬一些在應用中不容易構造或者比較複雜的對象,從而把測試與測試邊界以外的對象隔離開

我們可以自己編寫自定義的Mock對象實現mock技術,但是編寫自定義的Mock對象需要額外的編碼工作,同時也可能引入錯誤。現在實現mock技術的優秀開源框架有很多,Mockito就是一個優秀的用於單元測試的mock框架。Mockito已經在github上開源,詳細請點擊:https://github.com/mockito/mockito

除了Mockito以外,還有一些類似的框架,比如:

  • EasyMock:早期比較流行的MocK測試框架。它提供對接口的模擬,能夠通過錄制、回放、檢查三步來完成大體的測試過程,可以驗證方法的調用種類、次數、順序,可以令 Mock 對象返回指定的值或拋出指定異常
  • PowerMock:這個工具是在EasyMock和Mockito上擴展出來的,目的是爲了解決EasyMock和Mockito不能解決的問題,比如對static, final, private方法均不能mock。其實測試架構設計良好的代碼,一般並不需要這些功能,但如果是在已有項目上增加單元測試,老代碼有問題且不能改時,就不得不使用這些功能了
  • JMockit:JMockit 是一個輕量級的mock框架是用以幫助開發人員編寫測試程序的一組工具和API,該項目完全基於 Java 5 SE 的 java.lang.instrument 包開發,內部使用 ASM 庫來修改Java的Bytecode

Mockito已經被廣泛應用,所以這裏重點介紹Mockito。

Mockito使用舉例

這裏我們直接通過一個代碼來說明mockito對單元測試的幫助,代碼有三個類,分別如下:
Person類:

public class Person {

    private final int    id;
    private final String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

PersonDAO:

public interface PersonDao {

    Person getPerson(int id);

    boolean update(Person person);
}

PersonService:

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);
    }
}

在這裏,我們要進行測試的是PersonService類的update方法,我們發現,update方法依賴PersonDAO,在開發過程中,PersonDAO很可能尚未開發完成,所以我們測試PersonService的時候,所以該怎麼測試update方法呢?連接口都還沒實現,怎麼知道返回的是true還是false?在這裏,我們可以這樣認爲,單元測試的思路就是我們想在不涉及依賴關係的情況下測試代碼。這種測試可以讓你無視代碼的依賴關係去測試代碼的有效性。核心思想就是如果代碼按設計正常工作,並且依賴關係也正常,那麼他們應該會同時工作正常。所以我們的做法是mock一個PersonDAO對象,至於實際環境中,PersonDAO行爲是否能按照預期執行,比如update是否能成功,查詢是否返回正確的數據,就跟PersonService沒關係了。PersonService的單元測試只測試自己的邏輯是否有問題

下面編寫測試代碼:

public class PersonServiceTest {

    private PersonDao     mockDao;
    private PersonService personService;

    @Before
    public void setUp() throws Exception {
        //模擬PersonDao對象
        mockDao = mock(PersonDao.class);
        when(mockDao.getPerson(1)).thenReturn(new Person(1, "Person1"));
        when(mockDao.update(isA(Person.class))).thenReturn(true);

        personService = new PersonService(mockDao);
    }

    @Test
    public void testUpdate() throws Exception {
        boolean result = personService.update(1, "new name");
        assertTrue("must true", result);
        //驗證是否執行過一次getPerson(1)
        verify(mockDao, times(1)).getPerson(eq(1));
        //驗證是否執行過一次update
        verify(mockDao, times(1)).update(isA(Person.class));
    }

    @Test
    public void testUpdateNotFind() throws Exception {
        boolean result = personService.update(2, "new name");
        assertFalse("must true", result);
        //驗證是否執行過一次getPerson(1)
        verify(mockDao, times(1)).getPerson(eq(1));
        //驗證是否執行過一次update
        verify(mockDao, never()).update(isA(Person.class));
    }
}

我們對PersonDAO進行mock,並且設置stubbing,stubbing設置如下:

  • 當getPerson方法傳入1的時候,返回一個Person對象,否則默認返回空
  • 當調update方法的時候,返回true

我們驗證了兩種情況:

  • 更新id爲1的Person的名字,預期:能在DAO中找到Person並更新成功
  • 更新id爲2的Person的名字,預期:不能在DAO中找到Person,更新失敗

這樣,根據PersonService的update方法的邏輯,通過這兩個test case之後,我們認爲代碼是沒有問題的。mockito在這裏扮演了一個爲我們模擬DAO對象,並且幫助我們驗證行爲(比如驗證是否調用了getPerson方法及update方法)的角色

Android Studio工程配置Mockito

Android Studio中使用Mockito非常簡單,只需要在build.gradle文件中加入依賴即可。如圖:

dependencies {
    ...
    testCompile 'org.mockito:mockito-core:1.10.19'
    ...
}

Mockito使用方法

Mockito的使用,有詳細的api文檔,具體可以查看:http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html,下面是整理的一些常用的使用方式。

驗證行爲

一旦創建,mock會記錄所有交互,你可以驗證所有你想要驗證的東西

@Test
public void testVerify() throws Exception {
    //mock creation
    List mockedList = mock(List.class);

    //using mock object
    mockedList.add("one");
    mockedList.add("two");
    mockedList.add("two");
    mockedList.clear();

    //verification
    verify(mockedList).add("one");//驗證是否調用過一次 mockedList.add("one")方法,若不是(0次或者大於一次),測試將不通過
    verify(mockedList, times(2)).add("two");
    //驗證調用過2次 mockedList.add("two")方法,若不是,測試將不通過
    verify(mockedList).clear();//驗證是否調用過一次 mockedList.clear()方法,若沒有(0次或者大於一次),測試將不通過
}

Stubbing

@Test
public void testStubbing() throws Exception {
    //你可以mock具體的類,而不僅僅是接口
    LinkedList mockedList = mock(LinkedList.class);

    //設置樁
    when(mockedList.get(0)).thenReturn("first");
    when(mockedList.get(1)).thenThrow(new RuntimeException());

    //打印 "first"
    System.out.println(mockedList.get(0));

    //這裏會拋runtime exception
    System.out.println(mockedList.get(1));

    //這裏會打印 "null" 因爲 get(999) 沒有設置
    System.out.println(mockedList.get(999));

    //Although it is possible to verify a stubbed invocation, usually it's just redundant
    //If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
    //If your code doesn't care what get(0) returns, then it should not be stubbed. Not convinced? See here.
    verify(mockedList).get(0);
}

對於stubbing,有以下幾點需要注意:

  • 對於有返回值的方法,mock會默認返回null、空集合、默認值。比如,爲int/Integer返回0,爲boolean/Boolean返回false
  • stubbing可以被覆蓋,但是請注意覆蓋已有的stubbing有可能不是很好
  • 一旦stubbing,不管調用多少次,方法都會永遠返回stubbing的值
  • 當你對同一個方法進行多次stubbing,最後一次stubbing是最重要的

參數匹配

@Test
public void testArgumentMatcher() throws Exception {
    LinkedList mockedList = mock(LinkedList.class);
    //用內置的參數匹配器來stub
    when(mockedList.get(anyInt())).thenReturn("element");

    //打印 "element"
    System.out.println(mockedList.get(999));

    //你也可以用參數匹配器來驗證,此處測試通過
    verify(mockedList).get(anyInt());
    //此處測試將不通過,因爲沒調用get(33)
    verify(mockedList).get(eq(33));
}

如果你使用了參數匹配器,那麼所有參數都應該使用參數匹配器

 verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
 //上面是正確的,因爲eq返回參數匹配器

 verify(mock).someMethod(anyInt(), anyString(), "third argument");
 //上面將會拋異常,因爲第三個參數不是參數匹配器,一旦使用了參數匹配器來驗證,那麼所有參數都應該使用參數匹配

驗證準確的調用次數,最多、最少、從未等

@Test
public void testInvocationTimes() throws Exception {

    LinkedList mockedList = 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)
    verify(mockedList).add("once");
    verify(mockedList, times(1)).add("once");

    //驗證準確的調用次數
    verify(mockedList, times(2)).add("twice");
    verify(mockedList, times(3)).add("three times");

    //從未調用過. 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");
}

爲void方法拋異常

@Test
public void testVoidMethodsWithExceptions() throws Exception {

    LinkedList mockedList = mock(LinkedList.class);
    doThrow(new RuntimeException()).when(mockedList).clear();
    //下面會拋RuntimeException
    mockedList.clear();
}

驗證調用順序

@Test
public void testVerificationInOrder() throws Exception {
    // A. Single mock whose methods must be invoked in a particular order
    List singleMock = mock(List.class);

    //使用單個mock對象
    singleMock.add("was added first");
    singleMock.add("was added second");

    //創建inOrder
    InOrder inOrder = inOrder(singleMock);

    //驗證調用次數,若是調換兩句,將會出錯,因爲singleMock.add("was added first")是先調用的
    inOrder.verify(singleMock).add("was added first");
    inOrder.verify(singleMock).add("was added second");

    // 多個mock對象
    List firstMock = mock(List.class);
    List secondMock = mock(List.class);

    //using mocks
    firstMock.add("was called first");
    secondMock.add("was called second");

    //創建多個mock對象的inOrder
    inOrder = inOrder(firstMock, secondMock);

    //驗證firstMock先於secondMock調用
    inOrder.verify(firstMock).add("was called first");
    inOrder.verify(secondMock).add("was called second");
}

驗證mock對象沒有產生過交互

@Test
public void testInteractionNeverHappened() {
    List mockOne = mock(List.class);
    List mockTwo = mock(List.class);

    //測試通過
    verifyZeroInteractions(mockOne, mockTwo);

    mockOne.add("");
    //測試不通過,因爲mockTwo已經發生過交互了
    verifyZeroInteractions(mockOne, mockTwo);
}

查找是否有未驗證的交互

不建議過多使用,api原文:A word of warning: Some users who did a lot of classic, expect-run-verify mocking tend to use verifyNoMoreInteractions() very often, even in every test method. verifyNoMoreInteractions() is not recommended to use in every test method. verifyNoMoreInteractions() is a handy assertion from the interaction testing toolkit. Use it only when it’s relevant. Abusing it leads to overspecified, less maintainable tests.

@Test
public void testFindingRedundantInvocations() throws Exception {
    List mockedList = mock(List.class);
    //using mocks
    mockedList.add("one");
    mockedList.add("two");

    verify(mockedList).add("one");

    //驗證失敗,因爲mockedList.add("two")尚未驗證
    verifyNoMoreInteractions(mockedList);
}

@Mock註解

  • 減少代碼
  • 增強可讀性
  • 讓verify出錯信息更易讀,因爲變量名可用來描述標記mock對象
public class MockTest {

    @Mock
    List<String> mockedList;

    @Before
    public void initMocks() {
        //必須,否則註解無效
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testMock() throws Exception {
        mockedList.add("one");
        verify(mockedList).add("one");
    }
}

根據調用順序設置不同的stubbing

private interface MockTest {
    String someMethod(String arg);
}

@Test
public void testStubbingConsecutiveCalls() throws Exception {

    MockTest mock = mock(MockTest.class);
    when(mock.someMethod("some arg")).thenThrow(new RuntimeException("")).thenReturn("foo");

    //第一次調用,拋RuntimeException
    mock.someMethod("some arg");

    //第二次調用返回foo
    System.out.println(mock.someMethod("some arg"));

    //後續繼續調用,返回“foo”,以最後一個stub爲準
    System.out.println(mock.someMethod("some arg"));

    //下面是一個更簡潔的寫法
    when(mock.someMethod("some arg")).thenReturn("one", "two", "three");
}

doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod()等用法

@Test
public void testDoXXX() throws Exception {
    List mockedList = mock(List.class);
    doThrow(new RuntimeException()).when(mockedList).clear();
    //以下會拋異常
    mockedList.clear();
}

spy監視真正的對象

  • spy是創建一個拷貝,如果你保留原始的list,並用它來進行操作,那麼spy並不能檢測到其交互
  • spy一個真正的對象+試圖stub一個final方法,這樣是會有問題的
@Test
public void testSpy() throws Exception {
    List list = new LinkedList();
    List spy = spy(list);

    //可選的,你可以stub某些方法
    when(spy.size()).thenReturn(100);

    //調用"真正"的方法
    spy.add("one");
    spy.add("two");

    //打印one
    System.out.println(spy.get(0));

    //size()方法被stub了,打印100
    System.out.println(spy.size());

    //可選,驗證spy對象的行爲
    verify(spy).add("one");
    verify(spy).add("two");

    //下面寫法有問題,spy.get(10)會拋IndexOutOfBoundsException異常
    when(spy.get(10)).thenReturn("foo");
    //可用以下方式
    doReturn("foo").when(spy).get(10);
}

爲未stub的方法設置默認返回值

@Test
public void testDefaultValue() throws Exception {

    List listOne = mock(List.class, Mockito.RETURNS_SMART_NULLS);
    List listTwo = mock(List.class, new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {

            // TODO: 2016/6/13 return default value here
            return null;
        }
    });
}

參數捕捉

@Test
public void testCapturingArguments() throws Exception {
    List mockedList = mock(List.class);
    ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
    mockedList.add("John");
    //驗證後再捕捉參數
    verify(mockedList).add(argument.capture());
    //驗證參數
    assertEquals("John", argument.getValue());
}

真正的部分模擬(TODO:尚未搞清楚啥意思。。。)

    //you can create partial mock with spy() method:
    List list = spy(new LinkedList());

    //you can enable partial mock capabilities selectively on mocks:
    Foo mock = mock(Foo.class);
    //Be sure the real implementation is 'safe'.
    //If real implementation throws exceptions or depends on specific state of the object then you're in trouble.
    when(mock.someMethod()).thenCallRealMethod();

重置mocks

Don’t harm yourself. reset() in the middle of the test method is a code smell (you’re probably testing too much).

@Test
public void testReset() throws Exception {
    List mock = mock(List.class);
    when(mock.size()).thenReturn(10);
    mock.add(1);
    reset(mock);
    //從這開始,之前的交互和stub將全部失效
}

Serializable mocks

WARNING: This should be rarely used in unit testing.

@Test
public void testSerializableMocks() throws Exception {
    List serializableMock = mock(List.class, withSettings().serializable());
}

更多的註解:@Captor, @Spy, @InjectMocks

  • @Captor 創建ArgumentCaptor
  • @Spy 可以代替spy(Object).
  • @InjectMocks 如果此註解聲明的變量需要用到mock對象,mockito會自動注入mock或spy成員
//可以這樣寫
@Spy BeerDrinker drinker = new BeerDrinker();
//也可以這樣寫,mockito會自動實例化drinker
@Spy BeerDrinker drinker;

//會自動實例化
@InjectMocks LocalPub;

超時驗證

private interface TimeMockTest {
    void someMethod();
}

@Test
public void testTimeout() throws Exception {

    TimeMockTest mock = mock(TimeMockTest.class);
    //測試程序將會在下面這句阻塞100毫秒,timeout的時候再進行驗證是否執行過someMethod()
    verify(mock, timeout(100)).someMethod();
    //和上面代碼等價
    verify(mock, timeout(100).times(1)).someMethod();

    //阻塞100ms,timeout的時候再驗證是否剛好執行了2次
    verify(mock, timeout(100).times(2)).someMethod();

    //timeout的時候,驗證至少執行了2次
    verify(mock, timeout(100).atLeast(2)).someMethod();

    //timeout時間後,用自定義的檢驗模式驗證someMethod()
    VerificationMode yourOwnVerificationMode = new VerificationMode() {
        @Override
        public void verify(VerificationData data) {
            // TODO: 2016/12/4 implement me
        }
    };
    verify(mock, new Timeout(100, yourOwnVerificationMode)).someMethod();
}

查看是否mock或者spy

   Mockito.mockingDetails(someObject).isMock();
   Mockito.mockingDetails(someObject).isSpy();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章