Android單元測試(二)

在上一篇文章中我們介紹了Android單元測試入門所需了解的內容,本文接上文繼續學習單元測試相關框架。本文介紹了AssertJ、AssertJ-Android、Hamcrest和Robolectric框架的使用,Robolectric生命周期及Robolectric和PowerMock配合使用。

本文首發:http://yuweiguocn.github.io/
新浪微博:@于衛國

《月下獨酌》
花間一壺酒,獨酌無相親。
舉杯邀明月,對影成三人。
月既不解飲,影徒隨我身。
暫伴月將影,行樂須及春。
我歌月徘徊,我舞影零亂。
醒時同交歡,醉后各分散。
永結無情游,相期邈云漢。
-唐,李白

前言

本文要介紹的框架:

  • AssertJ:JAVA 流式斷言器,支持一條斷言語句對實際值同時斷言多個校驗點
  • AssertJ-Android:擴展自Assert,旨在讓它更容易測試Android
  • Hamcrest:Matchers匹配器
  • Robolectric:用于mock Android框架相關類

AssertJ

倉庫地址:https://github.com/joel-costigliola/assertj-core

AseertJ:JAVA 流式斷言器,什么是流式,常見的斷言器一條斷言語句只能對實際值斷言一個校驗點,而流式斷言器,支持一條斷言語句對實際值同時斷言多個校驗點。

添加依賴:

testCompile 'org.assertj:assertj-core:3.8.0'
  
//or for Java 7 projects
testCompile 'org.assertj:assertj-core:2.8.0'

添加靜態導入:

import static org.assertj.core.api.Assertions.*;
  
//或者如果你喜歡這樣的話:
import static org.assertj.core.api.Assertions.assertThat;  // main one
import static org.assertj.core.api.Assertions.atIndex; // for List assertions
import static org.assertj.core.api.Assertions.entry;  // for Map assertions
import static org.assertj.core.api.Assertions.tuple; // when extracting several properties at once
import static org.assertj.core.api.Assertions.fail; // use when writing exception tests
import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; // idem
import static org.assertj.core.api.Assertions.filter; // for Iterable/Array assertions
import static org.assertj.core.api.Assertions.offset; // for floating number assertions
import static org.assertj.core.api.Assertions.anyOf; // use with Condition
import static org.assertj.core.api.Assertions.contentOf; // use with File assertions
  
//對于android使用這個靜態導入
import static org.assertj.core.api.Java6Assertions.*;
// 靜態導入所有assertThat和實用方法
import static org.assertj.core.api.Assertions.*;
 
// 基礎斷言
assertThat(frodo.getName()).isEqualTo("Frodo");
assertThat(frodo).isNotEqualTo(sauron);
 
// 指定字符串鏈式斷言
assertThat(frodo.getName()).startsWith("Fro")
                           .endsWith("do")
                           .isEqualToIgnoringCase("frodo");
 
// 指定集合斷言
// in the examples below fellowshipOfTheRing is a List<TolkienCharacter>
assertThat(fellowshipOfTheRing).hasSize(9)
                               .contains(frodo, sam)
                               .doesNotContain(sauron);
 
// as() 用于指定錯誤信息
assertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);
 
// Java 8 異常斷言
assertThatThrownBy(() -> { throw new Exception("boom!"); }).hasMessage("boom!");
// ... or BDD style
Throwable thrown = catchThrowable(() -> { throw new Exception("boom!"); });
assertThat(thrown).hasMessageContaining("boom");
 
// using the 'extracting' feature to check fellowshipOfTheRing character's names (Java 7)
// 使用提取特性檢查字符串的名稱
assertThat(fellowshipOfTheRing).extracting("name")
                               .contains("Boromir", "Gandalf", "Frodo", "Legolas")
// same thing using a Java 8 method reference
//和java8的方法引用一樣
assertThat(fellowshipOfTheRing).extracting(TolkienCharacter::getName)
                               .doesNotContain("Sauron", "Elrond");
 
// 抽取多個值到一個組 in tuples (Java 7)
assertThat(fellowshipOfTheRing).extracting("name", "age", "race.name")
                               .contains(tuple("Boromir", 37, "Man"),
                                         tuple("Sam", 38, "Hobbit"),
                                         tuple("Legolas", 1000, "Elf"));
 
// 斷言之前過濾一個集合 in Java 7 ...
assertThat(fellowshipOfTheRing).filteredOn("race", HOBBIT)
                               .containsOnly(sam, frodo, pippin, merry);
// ... or in Java 8
assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o"))
                               .containsOnly(aragorn, frodo, legolas, boromir);
 
// 結合過濾和抽取功能
assertThat(fellowshipOfTheRing).filteredOn(character -> character.getName().contains("o"))
                               .containsOnly(aragorn, frodo, legolas, boromir)
                               .extracting(character -> character.getRace().getName())
                               .contains("Hobbit", "Elf", "Man");
 
// and many more assertions : iterable, stream, array, map, dates (java 7 and java 8), path, file, numbers, predicate, optional ...
 

對要斷言的對象輸入一個點會顯示所有可用的斷言。

AssertJ-Android

倉庫地址:https://github.com/square/assertj-android

擴展自Assert,旨在讓它更容易測試Android。

Assertj-Android和Junit和AssertJ之間的對比:

//ASSERTJ ANDROID
assertThat(view).isGone();
  
//REGULAR JUNIT
assertEquals(View.GONE, view.getVisibility());
  
//REGULAR ASSERTJ
assertThat(view.getVisibility()).isEqualTo(View.GONE);
  
//當斷言失敗,你可以直接看到失敗的原因
//Expected visibility <gone> but was <invisible>.
  
  
  
//ASSERTJ ANDROID
assertThat(layout).isVisible()
    .isVertical()
    .hasChildCount(4)
    .hasShowDividers(SHOW_DIVIDERS_MIDDLE);
  
//REGULAR JUNIT
assertEquals(View.VISIBLE, layout.getVisibility());
assertEquals(VERTICAL, layout.getOrientation());
assertEquals(4, layout.getChildCount());
assertEquals(SHOW_DIVIDERS_MIDDLE, layout.getShowDividers());
  
//REGULAR ASSERTJ
assertThat(layout.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(layout.getOrientation()).isEqualTo(VERTICAL);
assertThat(layout.getChildCount()).isEqualTo(4);
assertThat(layout.getShowDividers()).isEqualTo(SHOW_DIVIDERS_MIDDLE);

斷言包含幾乎所有你想要測試的對象,從LinearLayout到ActionBar、Fragment及MenuItem。以及support類庫所有東西。

添加依賴:

//Android module:
androidTestCompile 'com.squareup.assertj:assertj-android:1.1.1'
 
//support-v4 module:
androidTestCompile 'com.squareup.assertj:assertj-android-support-v4:1.1.1'
 
//Google Play Services module:
androidTestCompile 'com.squareup.assertj:assertj-android-play-services:1.1.1'
 
//appcompat-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-appcompat-v7:1.1.1'
 
//mediarouter-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-mediarouter-v7:1.1.1'
 
//gridlayout-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-gridlayout-v7:1.1.1'
 
//cardview-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-cardview-v7:1.1.1'
 
//recyclerview-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-recyclerview-v7:1.1.1'
 
//pallete-v7 module:
androidTestCompile 'com.squareup.assertj:assertj-android-pallete-v7:1.1.1'

靜態導入:

//android
import static org.assertj.android.api.Assertions.assertThat;
  
//support-v4
import static org.assertj.android.support.v4.api.Assertions.assertThat;
 
//Google Play Services
import static org.assertj.android.playservices.api.Assertions.assertThat;
 
//appcompat-v7
import static org.assertj.android.appcompat.v7.api.Assertions.assertThat;
 
//mediarouter-v7
import static org.assertj.android.mediarouter.v7.api.Assertions.assertThat;
 
//gridlayout-v7
import static org.assertj.android.gridlayout.v7.api.Assertions.assertThat;
 
//cardview-v7
import static org.assertj.android.cardview.v7.api.Assertions.assertThat;
 
//recyclerview-v7
import static org.assertj.android.recyclerview.v4.api.Assertions.assertThat;
 
//pallete-v7
import static org.assertj.android.pallete.v4.api.Assertions.assertThat;

Hamcrest

倉庫地址:https://github.com/hamcrest/JavaHamcrest

//添加依賴
testCompile "org.hamcrest:hamcrest-all:1.3"
  
  
//靜態導入
import static org.hamcrest.MatcherAssert.assertThat; 
import static org.hamcrest.Matchers.*; 

Hamcrest 是一個測試的框架,它提供了一套通用的匹配符 Matcher,靈活使用這些匹配符定義的規則,程序員可以更加精確的表達自己的測試思想,指定所想設定的測試條件。比如,有時候定義的測試數據范圍太精 確,往往是若干個固定的確定值,這時會導致測試非常脆弱,因為接下來的測試數據只要稍稍有變化,就可能導致測試失敗(比如 assertEquals( x, 10 ); 只能判斷 x 是否等于 10,如果 x 不等于 10, 測試失敗);有時候指定的測試數據范圍又不夠太精確,這時有可能會造成某些本該會導致測試不通過的數據,仍然會通過接下來的測試,這樣就會降低測試的價值。 Hamcrest 的出現,給程序員編寫測試用例提供了一套規則和方法,使用其可以更加精確的表達程序員所期望的測試的行為。

Hamcrest 常用的匹配器:

核心

  • anything - 總是匹配,如果你不關心測試下的對象是什么是有用的
  • describedAs - 添加一個定制的失敗表述裝飾器
  • is - 改進可讀性裝飾器 - 見下 “Sugar”

邏輯

  • allOf - 如果所有匹配器都匹配才匹配, short circuits (很難懂的一個詞,意譯是短路,感覺不對,就沒有翻譯)(像 Java &&)
  • anyOf - 如果任何匹配器匹配就匹配, short circuits (像 Java ||)
  • not - 如果包裝的匹配器不匹配器時匹配,反之亦然

對象

  • equalTo - 測試對象相等使用Object.equals方法
  • hasToString - 測試Object.toString方法
  • instanceOf, isCompatibleType - 測試類型
  • notNullValue, nullValue - 測試null
  • sameInstance - 測試對象實例

Beans

  • hasProperty - 測試JavaBeans屬性

集合

  • array - 測試一個數組元素test an array’s elements against an array of matchers
  • hasEntry, hasKey, hasValue - 測試一個Map包含一個實體,鍵或者值
  • hasItem, hasItems - 測試一個集合包含一個元素
  • hasItemInArray - 測試一個數組包含一個元素

數字

  • closeTo - 測試浮點值接近給定的值
  • greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo - 測試次序

文本

  • equalToIgnoringCase - 測試字符串相等忽略大小寫
  • equalToIgnoringWhiteSpace - 測試字符串忽略空白
  • containsString, endsWith, startsWith - 測試字符串匹配
// 比較50是否和50相等 
assertThat(50, equalTo(50)); 
// 50是否大于30并且小于60 
assertThat("錯誤",50, allOf(greaterThan(30), lessThan(60))); 
// 判斷字符串是否以.txt結尾 
assertThat("錯誤", "abc.txt", endsWith(".txt")); 

Robolectric

倉庫地址:https://github.com/robolectric/robolectric

添加依賴:

testCompile "org.robolectric:robolectric:3.3.2"

android的開發和編譯環境是JVM,需要的依賴SDK中的android.jar包。而android.jar包底層的方法都是stub的,沒有具體的實現。所以我們的項目編譯打包之后運行在device上沒問題,是因為device上有運行所需要的delvik環境,但是test運行的環境是JVM這時候如果用到了android.jar原生的方法就會導致異常java.lang.RuntimeException: Stub!
這時候就需要用到robolectric這個框架,這個框架通過對原生的類的替換,在調用到這些方法的時候攔截掉原有的調用,并用自己的實現替換被調用的方法。

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class MyActivityTest {
 
  @Test
  public void clickingButton_shouldChangeResultsViewText() throws Exception {
    Activity activity = Robolectric.setupActivity(MyActivity.class);
 
    Button button = (Button) activity.findViewById(R.id.press_me_button);
    TextView results = (TextView) activity.findViewById(R.id.results_text_view);
 
    button.performClick();
    assertThat(results.getText().toString(), equalTo("Testing Android Rocks!"));
  }
}

如果你使用的是Mac,你可能需要配置默認的Junit test runner,編輯Junit配置,修改working directory為$MODULE_DIR$,每個用到robolectric的junit都需要此配置。

Robolectric生命周期

首先,robolectric默認會從AndroidManifest.xml找到指定的application用于加載:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.myapp">
  <application android:name=".App"/>
</manifest>

這會加載com.myapp.App類,你可以在test目錄下創建用于測試的AndroidManifest.xml,并創建用于測試的Application,你可以實現TestLifecycleApplication接口,監聽重要事件的回調。

public class TestApp extends Application implements TestLifecycleApplication{
 
    @Override
    public void beforeTest(Method method) {
 
    }
 
    @Override
    public void prepareTest(Object test) {
 
    }
 
    @Override
    public void afterTest(Method method) {
 
    }
}
  
// src/test/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.myapp">
  <application android:name=".TestApp"/>
</manifest>
  
  
//在注解上使用manifest指定測試清單文件
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class,manifest = "src/test/AndroidManifest.xml")

當運行每個test時,其生命周期為:
1.創建application。
2.調用application.onCreate()。
3.調用application.beforeTest()。
4.調用application.prepareTest()。
5.運行測試用例。
6.調用application.onTerminate()。
7.調用application.afterTest()。

如果你打算在現有項目引入單元測試,建議添加用于測試的Application,這會你讓少走很多彎路。

Robolectric和PowerMock配合使用

//添加依賴
testCompile "org.robolectric:robolectric:3.3"
  
testCompile "org.powermock:powermock-module-junit4:1.6.4"
testCompile "org.powermock:powermock-module-junit4-rule:1.6.4"
testCompile "org.powermock:powermock-api-mockito:1.6.4"
testCompile "org.powermock:powermock-classloading-xstream:1.6.4"
  
  
  
  
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@PrepareForTest(Static.class)
public class DeckardActivityTest {
 
    @Rule
    public PowerMockRule rule = new PowerMockRule();
 
    @Test
    public void testStaticMocking() {
        PowerMockito.mockStatic(Static.class);
        Mockito.when(Static.staticMethod()).thenReturn("hello mock");
 
        assertTrue(Static.staticMethod().equals("hello mock"));
    }
}

PowerMockRule是用于代替@RunWith注解開啟PowerMock,因為我們已經使用RobolectricTestRunner指定了注解值。
@PowerMockIgnore用于忽略Mockito和Robolectric類庫,因為我們不應該mock它們自己。還有android的類,因為我們已經使用Robolectric處理了。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容