單元測試--Android單元測試學習總結(jié)(junit+Mockito+PowerMockito)

原文鏈接:川峰-Android單元測試學習總結(jié)

Android單元測試主要分為以下兩種

  • 本地單元測試(Junit Test), 本地單元測試是純java代碼的測試,只運行在本地電腦的JVM環(huán)境上,不依賴于Android框架的任何api, 因此執(zhí)行速度快,效率較高,但是無法測試Android相關(guān)的代碼。
  • 儀器化測試(Android Test),是針對Android相關(guān)代碼的測試,需要運行在真機設備或模擬器上,運行速度較慢,但是可以測試UI的交互以及對設備信息的訪問,得到接近真實的測試結(jié)果。
    在Android Studio中新建一個項目的時候,app的gradle中會默認添加單元測試的相關(guān)依賴庫:
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

其中testImplementation添加的依賴就是本地化測試庫, androidTestImplementation 添加的依賴則是Android環(huán)境下的測試庫,同時,在項目的工程目錄下也會默認創(chuàng)建好測試的目錄:


其中app/src/test/下面存放的是Junit本地測試代碼,app/src/androidTest/下面存放的是Android測試代碼。

一、本地單元測試

進行本地單元測試需要先了解一些基本的Junit注解:

注解名稱 含義
@Test 定義所在方法為單元測試方法,方法必須是public void
@Before 定義所在方法在每個測試用例執(zhí)行之前執(zhí)行一次, 用于準備測試環(huán)境(如: 初始化類,讀輸入流等),在一個測試類中,每個@Test方法的執(zhí)行都會觸發(fā)一次調(diào)用
@After 定義所在方法在每個測試用例執(zhí)行之后執(zhí)行一次,用于清理測試環(huán)境數(shù)據(jù),在一個測試類中,每個@Test方法的執(zhí)行都會觸發(fā)一次調(diào)用。
@BeforeClass 定義所在方法在測試類里的所有用例運行之前運行一次,方法必須是public static void,用于做一些耗時的初始化工作(如: 連接數(shù)據(jù)庫)
@AfterClass 定義所在方法在測試類里的所有用例運行之后運行一次,方法必須是public static void,用于清理數(shù)據(jù)(如: 斷開數(shù)據(jù)連接)
@Test (expected = Exception.class) 如果該測試方法沒有拋出Annotation中的Exception類型(子類也可以),則測試失敗
@Test(timeout=100) 如果該測試方法耗時超過100毫秒,則測試失敗,用于性能測試
@Ignore 或者 @Ignore(“太耗時”) 忽略當前測試方法,一般用于測試方法還沒有準備好,或者太耗時之類的
@FixMethodOrder 定義所在的測試類中的所有測試方法都按照固定的順序執(zhí)行,可以指定3個值,分別是DEFAULT、JVM、NAME_ASCENDING(字母順序)
@RunWith 指定測試類的測試運行器

更多可以參考Junit官網(wǎng):https://junit.org/junit4/

1. 創(chuàng)建測試類

接下來就可以創(chuàng)建測試類,除了可以手動創(chuàng)建測試類外,可以利用AS快捷鍵:將光標選中要創(chuàng)建測試類的類名上->按下ALT + ENTER->在彈出的彈窗中選擇Create Test

image

這會彈出下面的彈窗,或者鼠標在類名上右鍵選擇菜單Go to–>Test,也會彈出下面的彈窗

image

勾選需要進行測試的方法,會自動生成一個測試類:


image

如果勾選了@Before或@After的話也會自動給你生成對應的測試方法

接下來編寫測試方法,首先在要測試的目標類中寫幾個業(yè)務方法:

public class SimpleClass {

    public boolean isTeenager(int age) {
        if (age < 15) {
            return true;
        }
        return false;
    }

    public int add(int a, int b) {
        return a + b;
    }

    public String getNameById(int id) {
        if (id == 1) {
            return "小明";
        } else if (id == 2){
            return "小紅";
        }
        return "";
    }
}

然后,測試類:

@RunWith(JUnit4.class)
public class SimpleClassTest {
    private SimpleClass simpleClass;

    @Before
    public void setUp() throws Exception {
        simpleClass = new SimpleClass();
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void isTeenager() {
        Assert.assertFalse(simpleClass.isTeenager(20));
        Assert.assertTrue(simpleClass.isTeenager(14));
    }

    @Test
    public void add() {
        Assert.assertEquals(simpleClass.add(3, 2), 5);
        Assert.assertNotEquals(simpleClass.add(3, 2), 4);
    }

    @Test
    public void getNameById() {
        Assert.assertEquals(simpleClass.getNameById(1), "小明");
        Assert.assertEquals(simpleClass.getNameById(2), "小紅");
        Assert.assertEquals(simpleClass.getNameById(10), "");
    }
}

其中setUp()是自動生成的添加了@Before注解,這會在每個測試方法執(zhí)行前執(zhí)行,因此在這里創(chuàng)建一個目標對象,也可以選擇添加@BeforeClass注解但這時setUp()應該改為靜態(tài)的方法。然后在每個測試方法中編寫測試用例,這里使用org.junit.Assert包中的斷言方法,有很多assertXXX方法,可以自己選擇用來判斷目標方法的結(jié)果是否滿足預期。

2. Assert類中的常用斷言方法

方法 含義
assertNull(Object object) 斷言對象為空
assertNull(String message, Object object) 斷言對象為空,如果不為空拋出異常攜帶指定的message信息
assertNotNull(Object object) 斷言對象不為空
assertNotNull(Object object) 斷言對象不為空,如果為空拋出異常攜帶指定的message信息
assertSame(Object expected, Object actual) 斷言兩個對象引用的是同一個對象
assertSame(String message, Object expected, Object actual) 斷言兩個對象引用的是同一個對象,否則拋出異常攜帶指定的message信息
assertNotSame(Object expected, Object actual) 斷言兩個對象引用的不是同一個對象
assertNotSame(String message, Object expected, Object actual) 斷言兩個對象引用的不是同一個對象,否則拋出異常攜帶指定的message信息
assertTrue(boolean condition) 斷言結(jié)果為true
assertTrue(String message, boolean condition) 斷言結(jié)果為true, 為false時拋出異常攜帶指定的message信息
assertFalse(boolean condition) 斷言結(jié)果為false
assertFalse(String message, boolean condition) 斷言結(jié)果為false, 為true時拋出異常攜帶指定的message信息
assertEquals(long expected, long actual) 斷言兩個long 類型 expected 和 actual 的值相等
assertEquals(String message, long expected, long actual) 斷言兩個long 類型 expected 和 actual 的值相等,如不相等則拋異常攜帶指定message信息
assertEquals(Object expected, Object actual) 斷言兩個對象相等
assertEquals(String message, Object expected, Object actual) 斷言兩個對象相等,如果不相等則拋出異常攜帶指定的message信息
assertEquals(float expected, float actual, float delta) 斷言兩個 float 類型 expect 和 actual 在 delta 偏差值下相等,delta是誤差精度
assertEquals(String message, float expected, float actual, float delta) 斷言兩個 float 類型 expect 和 actual 在 delta 偏差值下相等,如果不相等則拋出異常攜帶指定的message信息
assertEquals(double expected, double actual, double delta) 斷言兩個 double 類型 expect 和 actual 在 delta 偏差值下相等
assertEquals(String message, double expected,double actual, double delta) 斷言兩個 double 類型 expect 和 actual 在 delta 偏差值下相等,如果不相等則拋出異常攜帶指定的message信息
assertArrayEquals(T[] expected, T[] actual) 斷言兩個相同類型的數(shù)組的元素一一對應相等
assertArrayEquals(String message, T[] expected, T[] actual) 斷言兩個相同類型的數(shù)組的元素一一對應相等,如果不相等則拋出異常攜帶指定的message信息
fail() 直接讓測試失敗
fail(String message) 直接讓測試失敗并給出message錯誤信息
assertThat(T actual, Matcher<? super T> matcher) 斷言actual和matcher規(guī)則匹配
assertThat(String reason, T actual, Matcher<? super T> matcher) 斷言actual和matcher規(guī)則匹配,否則拋出異常攜帶指定的reason信息

其中assertEquals的方法,都對應有一個assertNotEquals方法,這里不列了,assertThat是一個強大的方法:

 Assert.assertThat(1, is(1));
 Assert.assertThat(0, is(not(1)));
 Assert.assertThat("hello", startsWith("h"));
 List<String> items = new ArrayList<>();
 items.add("aaa");
 items.add("bbb");
 Assert.assertThat(items, hasItem("aaa"));

需要靜態(tài)導入org.hamcrest.Matchers類里面的方法,更多匹配方法請參考這個類。

3. 運行測試類

選中測試類右鍵Run運行,控制面板中就會顯示測試結(jié)果:


如果所有的測試用例都正常返回了預期的結(jié)果,則面板中左側(cè)每個測試方法前面會帶一個綠色的對勾,否則方法前面會變成紅色感嘆號并且控制面板會輸出異常,現(xiàn)在來改一個業(yè)務方法試一下:

    public boolean isTeenager(int age) {
        if (age < 15) {
            return false;
        }
        return false;
    }

這里將age < 15改為輸出false,假設這是我們在編碼的時候由于疏忽粗心造成的,然后運行測試類:


控制面板會告訴那一行出錯了:


也就是說這里沒有返回預期的結(jié)果,說明我們編寫的業(yè)務邏輯是有錯誤的,這時就需要改bug了。

4. 運行單個測試方法或多個測試類

上面是運行的整個測試類,如果要運行測試類的單個方法,則鼠標只選中某個要運行的測試方法,然后右鍵選擇Run即可。如果要同時運行多個測試類,而如果多個測試類在同一個包下面,則選中多個測試類所在的包目錄,然后右鍵選擇Run運行。否則可以通過下面的方式指定,創(chuàng)建一個空的測試類,然后添加注解:

@RunWith(Suite.class)
@Suite.SuiteClasses({SimpleClassTest.class, SimpleClass2Test.class})
public class RunMultiTest {
}

運行這個測試類就可以將指定的測試類的方法一起運行。

二、Mockito測試框架的使用

前面介紹的只能測試不涉及Android相關(guān)Api的java代碼用例,如果涉及到Android相關(guān)Api的時候,就不方便了,這時如果不依賴第三方庫的話可能需要使用儀器化測試跑到Android設備上去運行,于是有一些比較好的第三方的替代框架可以來模擬使用Android的代碼測試,Mockito就是基于依賴注入實現(xiàn)的一個測試框架。

1. Mock概念的理解

什么是Mock, 這個單詞的中文意思就是“模仿”或者“虛假”的意思,也就是要模仿一個對象,為啥要模仿?
在傳統(tǒng)的JUnit單元測試中,沒有消除在測試中對對象的依賴,如A對象依賴B對象方法,在測試A對象的時候,我們需要構(gòu)造出B對象,這樣子增加了測試的難度,或者使得我們對某些類的測試無法實現(xiàn)。這與單元測試的思路相違背。
還有一個主要的問題就是本地單元測試由于是運行本地JVM環(huán)境,無法依賴Android的api,只靠純Junit的測試環(huán)境很難模擬出完整的Android環(huán)境,導致無法測試Android相關(guān)的代碼,而Mock就能解決這個問題,通過Mock能夠很輕易的實現(xiàn)對象的模擬。

添加依賴:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    testImplementation 'org.mockito:mockito-core:2.19.0'
    ....
}
  1. Mockito中幾種Mock對象的方式

使用之前通過靜態(tài)方式導入會使用更方便:

 // 靜態(tài)導入會使代碼更簡潔
 import static org.mockito.Mockito.*;

直接mock一個對象:

    @Test
    public void testMock() {
        SimpleClass mockSimple = Mockito.mock(SimpleClass.class);
        assertNotNull(mockSimple);
    }

注解方式mock一個對象:

    @Mock
    SimpleClass simple;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testMock() {
        assertNotNull(simple);
    }

運行器方式mock一個對象:

@RunWith(MockitoJUnitRunner.class)
public class ExampleUnitTest {
    @Mock
    SimpleClass simple;
    
    @Test
    public void testMock() {
        assertNotNull(simple);
    }
}

MockitoRule方式mock一個對象:

public class ExampleUnitTest {
    @Mock
    SimpleClass simple;
    
    @Rule //<--使用@Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    
    @Test
    public void testMock() {
        assertNotNull(simple);
    }
}

3. 驗證行為

verify(T mock)函數(shù)的使用

verify(T mock)的作用是驗證發(fā)生的某些行為等同于verify(mock, times(1)) 例如:

@Test
public void testMock() {
     //創(chuàng)建mock對象
     List mockedList = mock(List.class);
     //使用mock對象
     mockedList.add("one");
     mockedList.clear();
    
     //驗證mockedList.add("one")是否被調(diào)用,如果被調(diào)用則當前測試方法通過,否則失敗
     verify(mockedList).add("one");
     //驗證 mockedList.clear()是否被調(diào)用,如果被調(diào)用則當前測試方法通過,否則失敗
     verify(mockedList).clear();
 }
@Test
public void testMock() {
    mock.someMethod("some arg");
    //驗證mock.someMethod("some arg")是否被調(diào)用,如果被調(diào)用則測試方法通過,否則失敗
    verify(mock).someMethod("some arg");
    
}

也就是說如果把調(diào)用的方法注釋掉,則運行testMock()方法就會失敗。

通過verify關(guān)鍵字,一旦mock對象被創(chuàng)建了,mock對象會記住所有的交互。然后你就可能選擇性的驗證你感興趣的交互。

通常需要配合一些測試方法來驗證某些行為,這些方法稱為"打樁方法"(Stub),打樁的意思是針對mock出來的對象進行一些模擬操作,如設置模擬的返回值或拋出異常等。

常見的打樁方法:

方法名 方法含義
doReturn(Object toBeReturned) 提前設置要返回的值
doThrow(Throwable… toBeThrown) 提前設置要拋出的異常
doAnswer(Answer answer) 提前對結(jié)果進行攔截
doCallRealMethod() 調(diào)用某一個方法的真實實現(xiàn)
doNothing() 設置void函數(shù)什么也不做
thenReturn(T value) 設置要返回的值
thenThrow(Throwable… throwables) 設置要拋出的異常
thenAnswer(Answer<?> answer) 對結(jié)果進行攔截

例如:

 @Test
 public void testMock() {
     // 你可以mock具體的類型,不僅只是接口
     List mockedList = mock(List.class);
     // 打測試樁
     when(mockedList.get(0)).thenReturn("first");
     doReturn("aaaa").when(mockedList).get(1);
     when(mockedList.get(1)).thenThrow(new RuntimeException());
     doThrow(new RuntimeException()).when(mockedList).clear();

     // 輸出“first”
     System.out.println(mockedList.get(0));
     // 因為get(999) 沒有打樁,因此輸出null, 注意模擬環(huán)境下這個地方是不會報IndexOutOfBoundsException異常的
     System.out.println(mockedList.get(999));
     // get(1)時會拋出異常
     System.out.println(mockedList.get(1));
     // clear會拋出異常
     mockedList.clear();
 }

doXXX和thenXXX使用上差不多,一個是調(diào)用方法之前設置好返回值,一個是在調(diào)用方法之后設置返回值。默認情況下,Mock出的對象的所有非void函數(shù)都有返回值,對象類型的默認返回的是null,例如返回int、boolean、String的函數(shù),默認返回值分別是0、false和null。

使用when(T methodCall)函數(shù)

打樁方法需要配合when(T methodCall)函數(shù),意思是使測試樁方法生效。當你想讓這個mock能調(diào)用特定的方法返回特定的值,那么你就可以使用它。

例如:

when(mock.someMethod()).thenReturn(10);
 //你可以使用靈活的參數(shù)匹配,例如 
 when(mock.someMethod(anyString())).thenReturn(10);

 //設置拋出的異常
 when(mock.someMethod("some arg")).thenThrow(new RuntimeException());

 //你可以對不同作用的連續(xù)回調(diào)的方法打測試樁:
 //最后面的測試樁(例如:返回一個對象:"foo")決定了接下來的回調(diào)方法以及它的行為。
 
 when(mock.someMethod("some arg"))
  .thenReturn("foo")//第一次調(diào)用someMethod("some arg")會返回"foo"
  .thenThrow(new RuntimeException());//第二次調(diào)用someMethod("some arg")會拋異常
  
 //可以用以下方式替代比較小版本的連貫測試樁:
 when(mock.someMethod("some arg"))
  .thenReturn("one", "two");
 //和下面的方式效果是一樣的
 when(mock.someMethod("some arg"))
  .thenReturn("one")
  .thenReturn("two");

 //比較小版本的連貫測試樁并且拋出異常:
 when(mock.someMethod("some arg"))
  .thenThrow(new RuntimeException(), new NullPointerException();

使用thenAnswer為回調(diào)做測試樁

when(mock.someMethod(anyString())).thenAnswer(new Answer() {
     Object answer(InvocationOnMock invocation) {
         Object[] args = invocation.getArguments();
         Object mock = invocation.getMock();
         return "called with arguments: " + args;
     }
 });

 // 輸出 : "called with arguments: foo"
 System.out.println(mock.someMethod("foo"));

使用doCallRealMethod()函數(shù)來調(diào)用某個方法的真實實現(xiàn)方法

注意,在Mock環(huán)境下,所有的對象都是模擬出來的,而方法的結(jié)果也是需要模擬出來的,如果你沒有為mock出的對象設置模擬結(jié)果,則會返回默認值,例如:

public class Person {
    public String getName() {
        return "小明";
    }
}

@Test
public void testPerson() {
    Person mock = mock(Person.class);
    //輸出null,除非設置發(fā)回模擬值when(mock.getName()).thenReturn("xxx");
    System.out.println(mock.getName());
}

因為getName()方法沒有設置模擬返回值,而getName()返回值是String類型的,因此直接調(diào)用的話會返回String的默認值null,所以上面代碼如果要想輸出getName()方法的真實返回值的話,需要設置doCallRealMethod():

 @Test
 public void testPerson() {
     Person mock = mock(Person.class);
     doCallRealMethod().when(mock).getName();
     //輸出“小明”
     System.out.println(mock.getName());
 }

使用doNothing()函數(shù)是為了設置void函數(shù)什么也不做

需要注意的是默認情況下返回值為void的函數(shù)在mocks中是什么也不做的但是,也會有一些特殊情況。如:

測試樁連續(xù)調(diào)用一個void函數(shù)時:

   doNothing().doThrow(new RuntimeException()).when(mock).someVoidMethod();
   //does nothing the first time:
   mock.someVoidMethod();
   //throws RuntimeException the next time:
   mock.someVoidMethod();

監(jiān)控真實的對象并且你想讓void函數(shù)什么也不做:

List list = new LinkedList();
List spy = spy(list);

//let's make clear() do nothing
doNothing().when(spy).clear();

spy.add("one");

//clear() does nothing, so the list still contains "one"
spy.clear();

使用doAnswer()函數(shù)測試void函數(shù)的回調(diào)

當你想要測試一個無返回值的函數(shù)時,可以使用一個含有泛型類Answer參數(shù)的doAnswer()函數(shù)做回調(diào)測試。假設你有一個void方法有多個回調(diào)參數(shù),當你想指定執(zhí)行某個回調(diào)時,使用thenAnswer很難實現(xiàn)了,如果使用doAnswer()將非常簡單,示例代碼如下:

MyCallback callback = mock(MyCallback.class);
Mockito.doAnswer(new Answer() {
    @Override
    public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
        //獲取第一個參數(shù)
        MyCallback call = invocation.getArgument(0);
        //指定回調(diào)執(zhí)行操作
        call.onSuccess();
        return null;
    }

}).when(mockedObject.requset(callback));

doAnswer(new Answer() {
         @Override
          public Object answer(InvocationOnMock invocation) throws Throwable {
              System.out.println("onSuccess answer");
              return null;
          }
 }).when(callback).onSuccess();
 
mockedObject.requset(callback)

需要使用doReturn函數(shù)代替thenReturn的情況

如當監(jiān)控真實的對象并且調(diào)用真實的函數(shù)帶來的影響時

List list = new LinkedList();
List spy = spy(list);

//不可能完成的:真實方法被調(diào)用的時候list仍是空的,所以spy.get(0)會拋出IndexOutOfBoundsException()異常
when(spy.get(0)).thenReturn("foo");

//這時你應該使用doReturn()函數(shù)
doReturn("foo").when(spy).get(0);

使用doThrow()函數(shù)來測試void函數(shù)拋出異常

SimpleClass mock = mock(SimpleClass.class);
doThrow(new RuntimeException()).when(mock).someVoidMethod();
mock.someVoidMethod();

總之使用doThrow(), doAnswer(), doNothing(), doReturn() and doCallRealMethod() 這些函數(shù)時可以在適當?shù)那闆r下調(diào)用when()來解決一些問題., 如當你需要下面這些功能時這是必須的:

  • 測試void函數(shù)
  • 在受監(jiān)控的對象上測試函數(shù)
  • 不只一次的測試同一個函數(shù),在測試過程中改變mock對象的行為

4. 驗證方法的調(diào)用次數(shù)

需要配合使用一些方法

方法 含義
times(int wantedNumberOfInvocations) 驗證調(diào)用方法的次數(shù)
never() 驗證交互沒有發(fā)生,相當于times(0)
only() 驗證方法只被調(diào)用一次,相當于times(1)
atLeast(int minNumberOfInvocations) 至少進行n次驗證
atMost(int maxNumberOfInvocations) 至多進行n次驗證
after(long millis) 在給定的時間后進行驗證
timeout(long millis) 驗證方法執(zhí)行是否超時
description(String description) 驗證失敗時輸出的內(nèi)容
verifyZeroInteractions 驗證mock對象沒有交互

例如:

mock.someMethod("some arg");
mock.someMethod("some arg");
//驗證mock.someMethod("some arg")被連續(xù)調(diào)用兩次,即如果沒有調(diào)用兩次則驗證失敗
verify(mock, times(2)).someMethod("some arg");

//注意,下面三種是等價的,都是驗證someMethod()被只調(diào)用一次

verify(mock).someMethod("some arg");
verify(mock, times(1)).someMethod("some arg");
verify(mock, only()).someMethod("some arg");
mPerson.getAge();
mPerson.getAge();
//驗證至少調(diào)用2次
verify(mPerson, atLeast(2)).getAge();
//驗證至多調(diào)用2次
verify(mPerson, atMost(2)).getAge();
//下面兩種等價,驗證調(diào)用次數(shù)為0
verify(mPerson, never()).getAge();
verify(mPerson, times(0)).getAge();
mPerson.getAge();
mPerson.getAge();
long current = System.currentTimeMillis();
System.out.println(current );
//延時1s后驗證mPerson.getAge()是否被執(zhí)行了2次
verify(mPerson, after(1000).times(2)).getAge();
System.out.println(System.currentTimeMillis() - current);
 mPerson.getAge();
 mPerson.getAge();
 //驗證方法在100ms超時前被調(diào)用2次
 verify(mPerson, timeout(100).times(2)).getAge();
  @Test
  public void testVerifyZeroInteractions() {
      Person person = mock(Person.class);
      person.eat("a");
      //由于person對象發(fā)生了交互,所以這里驗證失敗,把上面的調(diào)用注釋掉這里就會驗證成功
      verifyZeroInteractions(person);
      //可以驗證多個對象沒有交互
      //verifyZeroInteractions(person,person2 );
  }
  @Test
  public void testVerifyZeroInteractions() {
      Person person = mock(Person.class);
      person.eat("a");
      verify(person).eat("a");
      //注意,這將會無法到達驗證目的,不能跟verify()混用
      verifyZeroInteractions(person,person2 );
  }

5. 參數(shù)匹配器 (matchers)

Mockito以自然的java風格來驗證參數(shù)值: 使用equals()函數(shù)。有時,當需要額外的靈活性時你可能需要使用參數(shù)匹配器,也就是argument matchers :

// 使用內(nèi)置的anyInt()參數(shù)匹配器
 when(mockedList.get(anyInt())).thenReturn("element");

 // 使用自定義的參數(shù)匹配器( 在isValid()函數(shù)中返回你自己的匹配器實現(xiàn) )
 when(mockedList.contains(argThat(isValid()))).thenReturn("element");

 // 輸出element
 System.out.println(mockedList.get(999));

 // 你也可以驗證參數(shù)匹配器
 verify(mockedList).get(anyInt());

常用的參數(shù)匹配器:

方法名 含義
anyObject() 匹配任何對象
any(Class type) 與anyObject()一樣
any() 與anyObject()一樣
anyBoolean() 匹配任何boolean和非空Boolean
anyByte() 匹配任何byte和非空Byte
anyCollection() 匹配任何非空Collection
anyDouble() 匹配任何double和非空Double
anyFloat() 匹配任何float和非空Float
anyInt() 匹配任何int和非空Integer
anyList() 匹配任何非空List
anyLong() 匹配任何long和非空Long
anyMap() 匹配任何非空Map
anyString() 匹配任何非空String
contains(String substring) 參數(shù)包含給定的substring字符串
argThat(ArgumentMatcher matcher) 創(chuàng)建自定義的參數(shù)匹配模式
eq(T value) 匹配參數(shù)等于某個值

一些示例代碼:

    @Test
    public void testPersonAny(){
        when(mPerson.eat(any(String.class))).thenReturn("米飯");
        //或:
        when(mPerson.eat(anyString())).thenReturn("米飯");
        //輸出米飯
        System.out.println(mPerson.eat("面條"));
    }

    @Test
    public void testPersonContains(){
        when(mPerson.eat(contains("面"))).thenReturn("面條");
        //輸出面條
        System.out.println(mPerson.eat("面"));
    }

    @Test
    public void testPersonArgThat(){
        //自定義輸入字符長度為偶數(shù)時,輸出面條。
        when(mPerson.eat(argThat(new ArgumentMatcher<String>() {
            @Override
            public boolean matches(String argument) {
                return argument.length() % 2 == 0;
            }
        }))).thenReturn("面條");
        //輸出面條
        System.out.println(mPerson.eat("1234"));
    }

需要注意的是,如果你打算使用參數(shù)匹配器,那么所有參數(shù)都必須由匹配器提供。例如:

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
// 上述代碼是正確的,因為eq()也是一個參數(shù)匹配器

verify(mock).someMethod(anyInt(), anyString(), "third argument");
// 上述代碼是錯誤的, 因為所有參數(shù)必須由匹配器提供,而參數(shù)"third argument"并非由參數(shù)匹配器提供,因此會拋出異常

像anyObject(), eq()這樣的匹配器函數(shù)不會返回匹配器。它們會在內(nèi)部將匹配器記錄到一個棧當中,并且返回一個假的值,通常為null。

6. 使用InOrder驗證執(zhí)行執(zhí)行順序

驗證執(zhí)行執(zhí)行順序主要使用InOrder函數(shù)
如,驗證mock一個對象的函數(shù)執(zhí)行順序:

    @Test
    public void testInorder() {
        List<String> singleMock = mock(List.class);

        singleMock.add("小明");
        singleMock.add("小紅");

        // 為該mock對象創(chuàng)建一個inOrder對象
        InOrder inOrder = inOrder(singleMock);

        // 驗證add函數(shù)首先執(zhí)行的是add("小明"),然后才是add("小紅"),否則測試失敗
        inOrder.verify(singleMock).add("小明");
        inOrder.verify(singleMock).add("小紅");
    }

驗證多個mock對象的函數(shù)執(zhí)行順序:

    @Test
    public void testInorderMulti() {
        List<String> firstMock = mock(List.class);
        List<String> secondMock = mock(List.class);

        firstMock.add("小明");
        secondMock.add("小紅");

        // 為這兩個Mock對象創(chuàng)建inOrder對象
        InOrder inOrder = inOrder(firstMock, secondMock);

        // 驗證它們的執(zhí)行順序
        inOrder.verify(firstMock).add("小明");
        inOrder.verify(secondMock).add("小紅");
    }

驗證執(zhí)行順序是非常靈活的,你不需要一個一個的驗證所有交互,只需要驗證你感興趣的對象即可。 你可以選擇單個mock對象和多個mock對象混合著來,也可以僅通過那些需要驗證順序的mock對象來創(chuàng)建InOrder對象。

7. 使用Spy監(jiān)控真實對象

監(jiān)控真實對象使用spy()函數(shù)生成,或者也可以像@Mock那樣使用@Spy注解來生成一個監(jiān)控對象, 當你你為真實對象創(chuàng)建一個監(jiān)控(spy)對象后,在你使用這個spy對象時真實的對象也會也調(diào)用,除非它的函數(shù)被stub了。盡量少使用spy對象,使用時也需要小心形式。

    @Test
    public void testSpy() {
        List<String> list = new ArrayList<>();
        List<String> spy = spy(list);

        // 你可以選擇為某些函數(shù)打樁
        when(spy.size()).thenReturn(100);

        // 調(diào)用真實對象的函數(shù)
        spy.add("one");
        spy.add("two");

        // 輸出第一個元素"one"
        System.out.println(spy.get(0));

        // 因為size()函數(shù)被打樁了,因此這里返回的是100
        System.out.println(spy.size());

        // 驗證交互
        verify(spy).add("one");
        verify(spy).add("two");
    }

使用@Spy生成監(jiān)控對象:

    @Spy
    Person mSpyPerson;

    @Test
    public void testSpyPerson() {
        //將會輸出Person 類中g(shù)etName()的真實實現(xiàn),而不是null
        System.out.println(mSpyPerson.getName());
    }

理解監(jiān)控真實對象非常重要!有時,在監(jiān)控對象上使用when(Object)來進行打樁是不可能或者不切實際的。因此,當使用監(jiān)控對象時請考慮doReturn|Answer|Throw()函數(shù)族來進行打樁。例如:

List list = new LinkedList();
List spy = spy(list);

// 不可能實現(xiàn) : 因為當調(diào)用spy.get(0)時會調(diào)用真實對象的get(0)函數(shù),
// 此時會發(fā)生IndexOutOfBoundsException異常,因為真實List對象是空的
 when(spy.get(0)).thenReturn("foo");

// 你需要使用doReturn()來打樁
doReturn("foo").when(spy).get(0);

8. 使用ArgumentCaptor進行參數(shù)捕獲

參數(shù)捕獲主要為了下一步的斷言做準備,示例代碼:

    @Test
    public void argumentCaptorTest() {
        List<Object> mock = mock(List.class);
        mock.add("John");
         //構(gòu)建要捕獲的參數(shù)類型,這里是String
        ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
        //在verify方法的參數(shù)中調(diào)用argument.capture()方法來捕獲輸入的參數(shù)
        verify(mock).add(argument.capture());
        //驗證“John”參數(shù)捕獲
        assertEquals("John", argument.getValue());
    }
    @Test
    public void argumentCaptorTest2() {
        List<Object> mock = mock(List.class);
        mock.add("Brian");
        mock.add("Jim");

        ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
        verify(mock, times(2)).add(argument.capture());
        //如果又多次參數(shù)調(diào)用,argument.getValue()捕獲到的是最后一次調(diào)用的參數(shù)
        assertEquals("Jim", argument.getValue());
        //如果要獲取所有的參數(shù)值可以調(diào)用argument.getAllValues()
        assertArrayEquals(new Object[]{"Brian","Jim"}, argument.getAllValues().toArray());
    }

9. 使用@InjectMocks自動注入依賴對象

有時我們要測試的對象內(nèi)部需要依賴另一個對象,例如:

public class User {
    private Address address;

    public void setAddress(Address address) {
        this.address = address;
    }

    public String getAddress() {
        return address.getDetail();
    }
}
public class Address {
    public String getDetail() {
        return "detail Address";
    }
}

User類內(nèi)部需要依賴Address類,當我們測試的時候需要mock出這兩個對象,然后將Address對象傳入到User當中,這樣如果依賴的對象多了的話就相當麻煩,Mockito 提供了可以不用去手動注入對象的方法,首先使用@InjectMocks注解需要被注入的對象,如User,然后需要被依賴注入的對象使用@Mock或@Spy注解,之后Mockito 會自動完成注入過程,例如:

    @InjectMocks
    User mTestUser;
    @Mock
    Address mAddress;
    @Test
    public void argumentInjectMock() {
        when(mAddress.getDetail()).thenReturn("浙江杭州");
        System.out.println(mTestUser.getAddress());
    }

這樣就不用關(guān)心為User 設置Address ,只要為User需要依賴的類添加注解就可以了,然后直接將重點放到測試方法的編寫上。

或者使用@Spy監(jiān)控真實對象注入也可以:

    @InjectMocks
    User mTestUser;
    @Spy
    Address mAddress;
    @Test
    public void argumentInjectMock() {
        //  when(mAddress.getDetail()).thenReturn("浙江杭州");
        System.out.println(mTestUser.getAddress());
    }

其他:

連續(xù)調(diào)用的另一種更簡短的版本:

// 第一次調(diào)用時返回"one",第二次返回"two",第三次返回"three"
 when(mock.someMethod("some arg")).thenReturn("one", "two", "three");

參考:Mockito 中文文檔

三、PowerMockito框架使用

Mockito框架基本滿足需求但是有一些局限性,如對static、final、private等方法不能mock,PowerMockito就可以解決這些問題,PowerMockito是一個擴展了其它如EasyMock等mock框架的、功能更加強大的框架。PowerMock使用一個自定義類加載器和字節(jié)碼操作來模擬靜態(tài)方法,構(gòu)造函數(shù),final類和方法,私有方法,去除靜態(tài)初始化器等等。

添加依賴:

    testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
    testImplementation 'org.powermock:powermock-module-junit4-rule:2.0.2'
    testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
    testImplementation 'org.powermock:powermock-classloading-xstream:2.0.2'

1. 普通Mock的方式

目標類:

public class CommonExample {
    public boolean callArgumentInstance(File file) {
        return file.exists();
    }
}

測試類:

public class CommonExamplePowerMockTest {
    @Test
    public void testCallArgumentInstance() {
        File file = PowerMockito.mock(File.class);
        CommonExample commonExample = new CommonExample();
        PowerMockito.when(file.exists()).thenReturn(true);
        Assert.assertTrue(commonExample.callArgumentInstance(file));
    }
 }

普通Mock方式是外部傳遞Mock參數(shù),基本上和單獨使用Mockito是一樣的,使用純Mockito的api也可以完成這個測試。

2. Mock方法內(nèi)部new出來的對象

public class CommonExample {
    public boolean callArgumentInstance(String path) {
        File file = new File(path);
        return file.exists();
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest(CommonExample.class)
public class CommonExamplePowerMockTest {
    @Test
    public void callCallArgumentInstance2() throws Exception {
        File file = PowerMockito.mock(File.class);
        CommonExample commonExample = new CommonExample();
        PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file);
        PowerMockito.when(file.exists()).thenReturn(true);
        Assert.assertTrue(commonExample.callArgumentInstance("aaa"));
    }
}

跟前面有一點區(qū)別的就是,這里要測試的方法內(nèi)部創(chuàng)建了File對象,這時需要通過PowerMockito.whenNew(File.class).withArguments("aaa").thenReturn(file)方法模擬創(chuàng)建File的操作,當File類以aaa的參數(shù)創(chuàng)建的時候返回已經(jīng)mock出來的file對象。同時這時需要在測試類上添加注解@RunWith(PowerMockRunner.class)和@PrepareForTest(CommonExample.class),注意是在類上面添加,不是在方法上,一開始在方法上添加時提示找不到測試方法,@PrepareForTest()括號里面指定的是要測試的目標類。

3. Mock普通對象的final方法

public class CommonExample {
    public boolean callFinalMethod(DependencyClass dependency) {
        return dependency.isValidate();
    }
}

public class DependencyClass {
    public final boolean isValidate() {
        // do something
        return false;
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({CommonExample.class, DependencyClass.class})
public class CommonExamplePowerMockTest {
    @Test
    public void callFinalMethod() {
        DependencyClass dependency = PowerMockito.mock(DependencyClass.class);
        CommonExample commonExample = new CommonExample();
        PowerMockito.when(dependency.isValidate()).thenReturn(true);
        Assert.assertTrue(commonExample.callFinalMethod(dependency));
    }
}

同樣這里mock出來需要依賴的類的對象,然后傳遞給調(diào)用方法,這里同樣需要添加@RunWith和@PrepareForTest,@PrepareForTest可以指定多個目標類,但是這里如果你只需要測試final的話,只添加DependencyClass.class一個就可以了。

4. Mock普通類的靜態(tài)方法

public final class Utils {
    public static String getUUId() {
        return UUID.randomUUID().toString();
    }
}

public class CommonExample {
    public String printUUID() {
        return Utils.getUUId();
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest(Utils.class)
public class StaticUnitTest {

    @Before
    public void setUp() throws Exception {
        PowerMockito.mockStatic(Utils.class);
    }

    @Test
    public void getUUId() {
        PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
        CommonExample commonExample = new CommonExample();
        assertThat(commonExample.printUUID(), is("FAKE UUID"));
    }
}

同樣需要指定@RunWith和@PrepareForTest,@PrepareForTest中指定靜態(tài)方法所在的類,測試靜態(tài)方法之前需要調(diào)用PowerMockito.mockStatic()方法來mock靜態(tài)類,然后就通過when().thenReturn()方法指定靜態(tài)方法的模擬返回值即可。

5. verify靜態(tài)方法的調(diào)用次數(shù)

@Test
    public void testVerify() {
        PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
        CommonExample commonExample = new CommonExample();
        System.out.println(commonExample.printUUID());
        PowerMockito.verifyStatic(Utils.class);
        Utils.getUUId();
    }

靜態(tài)方法通過PowerMockito.verifyStatic(Class c)進行驗證,不過這里跟Mocktio有一點區(qū)別的是需要在這個方法的后面再調(diào)用一次靜態(tài)方法,否則不行。這里PowerMockito.verifyStatic(Utils.class)其實等同于PowerMockito.verifyStatic(Utils.class, times(1)),如果想驗證超過一次的,那么:

    @Test
    public void testVerify() {
        PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
        CommonExample commonExample = new CommonExample();
        System.out.println(commonExample.printUUID());
        System.out.println(commonExample.printUUID());
        PowerMockito.verifyStatic(Utils.class, Mockito.times(2));
        Utils.getUUId();
    }

這時PowerMockito.verifyStatic()第一個參數(shù)指定靜態(tài)方法類的Class,第二個參數(shù)接收一個VerificationMode類型的參數(shù),因此傳遞Mockito中的任何驗證方法次數(shù)的函數(shù)都可以,Mockito中的驗證函數(shù)會返回的是一個VerificationMode類型。同樣在PowerMockito.verifyStatic方法后面要調(diào)用一次要驗證的靜態(tài)方法,總感覺這里很奇怪。。。

6. 使用真實返回值

如果在測試的過程中又遇到不需要mock出來的靜態(tài)方法的模擬返回值,而是需要真實的返回值,怎么辦呢,其實跟Mockito一樣,PowerMockito同樣提供thenCallRealMethod或者doCallRealMethod方法:

    @Test
    public void testRealCall() throws Exception {
        PowerMockito.when(Utils.getUUId()).thenReturn("FAKE UUID");
        //...
        PowerMockito.when(Utils.getUUId()).thenCallRealMethod();
        //與下面等價
        //PowerMockito.doCallRealMethod().when(Utils.class, "getUUId");
        System.out.println(Utils.getUUId());
    }

或者直接使用spy監(jiān)控真實對象也可以:

    @Test
    public void testRealCall() {
        PowerMockito.spy(Utils.class);
        System.out.println(Utils.getUUId());
    }

7. Mock私有方法

public class CommonExample {
    public boolean callPrivateMethod() {
        return isExist();
    }
    private boolean isExist() {
        return false;
    }
 }
@RunWith(PowerMockRunner.class)
@PrepareForTest(CommonExample.class)
public class PrivateUnitTest {
    @Test
    public void testCallPrivateMethod() throws Exception {
        CommonExample commonExample = PowerMockito.mock(CommonExample.class);
        PowerMockito.when(commonExample.callPrivateMethod()).thenCallRealMethod();
        PowerMockito.when(commonExample, "isExist").thenReturn(true);
        Assert.assertTrue(commonExample.callPrivateMethod());
    }
}

在使用上跟純Mockito的沒有太大區(qū)別,只不過Mock私有方法是通過下面的api實現(xiàn)的:

PowerMockito.when(Object instance, String methodName, Object... arguments)
在PowerMockito中when函數(shù)與Mockito相比,最大的變化就是多了一些傳遞String類型的methodName的重載方法,這樣在使用上幾乎無所不能了。

8. Mock普通類的私有變量

public class CommonExample {
    private static final int STATE_NOT_READY = 0;
    private static final int STATE_READY = 1;
    private int mState = STATE_NOT_READY;
    
    public boolean doSomethingIfStateReady() {
        if (mState == STATE_READY) {
            // DO some thing
            return true;
        } else {
            return false;
        }
    }
 }
    @Test
    public void testDoSomethingIfStateReady() throws Exception {
        CommonExample sample = new CommonExample();
        Whitebox.setInternalState(sample, "mState", 1);
        assertThat(sample.doSomethingIfStateReady(), is(true));
    }

通過Whitebox.setInternalState來改變私有成員變量,這種情況下不需要指定@RunWith和@PrepareForTest。

9. 對靜態(tài)void方法進行Mock

public class CommonExample {
    public static void doSomething(String a) {
        System.out.println("doSomething"+a);
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({CommonExample.class})
public class StaticUnitTest {
   @Test
    public void testStaticVoid() throws Exception {
        PowerMockito.mockStatic(CommonExample.class);
        PowerMockito.doNothing().when(CommonExample.class, "doSomething", Mockito.any());
        CommonExample.doSomething("aaa");
    }
}

默認情況下通過PowerMockito.mockStatic的靜態(tài)類的void的方法是什么也不做的,但是可以顯示的執(zhí)行doNothing, 上面的代碼將doNothing那行注釋掉也是什么也不做的。那如果想做一些事而不是doNothing呢,跟Mockito一樣,采用doAnswer:

    @Test
    public void testStaticVoid() throws Exception {
        PowerMockito.mockStatic(CommonExample.class);
        PowerMockito.doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                System.out.println(invocation.getArguments()[0]);
                return null;
            }
        }).when(CommonExample.class, "doSomething", Mockito.any());
        CommonExample.doSomething("aaa");
    }

10. Mock系統(tǒng)的final靜態(tài)類

public class CommonExample {
    public int callSystemStaticMethod(int a, int b) {
        return Math.max(a, a+b);
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest(CommonExample.class)
public class StaticUnitTest {
    @Test
    public void callSystemStaticMethod() {
        CommonExample commonExample = new CommonExample();
        PowerMockito.mockStatic(Math.class);
        PowerMockito.when(Math.max(anyInt(), anyInt())).thenReturn(100);
        Assert.assertEquals(100, commonExample.callSystemStaticMethod(10, -5));
    }
}

@PrepareForTest中添加調(diào)用系統(tǒng)類所在的類,這里需要注意的是如果你使用PowerMockito來mock系統(tǒng)靜態(tài)final類,則gradle依賴中不能再添加單純Mockito的依賴庫,否則這里將不能mock成功,會提示Mockito can not mock/spy final class, 因為PowerMockito本身已經(jīng)有對Mockito的依賴庫支持了,所以只依賴PowerMockito就可以了。除了系統(tǒng)靜態(tài)final類的情況,其他的情況下PowerMockito和Mockito可以同時依賴(我測試是沒有問題的)。另外單純的Mockito新版本中也支持對 final 類 final 方法的 Mock,但是需要添加配置文件并不友好。

四、Robolectric測試框架的使用

由于Robolectric部分的內(nèi)容比較長,所以單獨放了一篇文章中:Android單元測試框架Robolectric的學習使用

五、Espresso測試框架的使用

Espresso是用于Android儀器化測試的測試框架,是谷歌官方主推的一個測試庫。由于Espresso部分的內(nèi)容也比較長,所以單獨放了一篇文章中:Espresso測試框架的使用

原文鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容