Android自動化測試

Instrumentation介紹

  • Instrumentation是個什么東西?
  • Instrumentation測試
  • Instrumentation原理介紹

一、Instrumentation是個什么東西?

  • Instrumentation是位于android.app包下,與Activity處于同一級目錄,它是Android系統中一系列控制方法的集合(俗稱hook)。這些hook可以在正常的生命周期之外控制Android控件的運行,也可控制Android如何加載應用程序。
  • 可以說Instrumentation就是AndroidSDK在Junit上的擴展,提供了AndroidTestCase類及其系列子類,其中最重要的一個類是ActivityInstrumentationTestCase2。
  • Instrumentation將在任何應用程序啟動之前初始化,通過它來檢測系統與應用程序之間的所有的交互。
  • 通過Instrumentation啟動時,測試程序與被測應用運行在同一進程的不同線程中。
  • application package 與 test package 處于同一個進程。
  • Android測試框架基于JUnit,因此可以直接使用JUnit來測試一些與Android平臺不相關的類,或者使用Android的JUnit擴展來測試Android組件;
  • Android JUnit擴展提供了對Android特定組件(Activity,Service等)的測試支持,也就是對mock的支持;

二、如何使用Instrumentation進行單元測試?

  • 環境配置:

在AndroidMainfast.xml文件中加入一下代碼:

<instrumentation  
    android:name="android.test.InstrumentationTestRunner"

    android:targetPackage="com.xxx.xxx" />

注意:targetPackage是被測應用的包名

或者在App module gradle中增加以下下配置:

defaultConfig {
    testInstrumentationRunner "android.support.test.InstrumentationTestRunner"
}
  • 使用Instrumentation進行自動化測試步驟:

1、 啟動應用

Intent intent = new Intent();
intent.setClassName(“packageName”,”className”);
intent,setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MainActivity activity = (MainActivity)getInstrumentation().startActivitySync(intent);

2、編輯界面:

Button button = activity.findViewById(R.id.btn);

3、結果提交:

button.performClick();

4、界面跳轉:

ActivityMonitor monitor = getInstrumentation().addMonitor(String cls, ActivityResult result, boolean block);
ChangeActivity ca = (ChangeActivity)getInstrumentation().waitForMonitor(monitor);

5、驗證結果:

assertTrue(ca != null);

三、Instrumentation源碼分析?

1)、測試框架如何運行?

想要了解測試框架運行過程需從InstrumentationTestRunner類入手,此類為Android測試框架啟動入口。
整個運行流程如下:

  • 創建線程:通過InstrumentationTestRunner創建Instrumentation專屬線程InstrumentationThread。
  • 獲取線程:通過InstrumentationTestRunner獲取線程
  • 執行用例:通過AndroidTestRunner對象循環執行測試用例

通過Instrumentation框架可以直接操作activity的生命周期函數。
測試activity的基本測試類為InstrumentationTestCase,它提供了Instrumentation接口給TestCase的子類,為了支持activity的測試,InstrumentationtTestCase提供了下面功能:

  • 生命周期的控制:使用Instrumentation可以啟動,暫停,終止被測試的activity。
  • Dependency Injection(依賴注入):Instrumentation允許創建一些mock對象,如Context,Application來幫助測試activity,從而幫助你控制測試環境并和實際應用的其他部分隔離開來。
  • 用戶界面交互:你可以使用Instrumentation向UI發送按鍵觸摸事件。

以下是由TestCase派生而來的測試類:

  • ActivityInstrumationTestCase2 : 通常用于多個activity的功能測試,它使用正常的系統框架來運行activity(使用應用程序本身),并使用正常系統Context(非Mock)來測試activity的功能,允許你創建一些mock Intent用來測試activity的響應,這種case不允許使用mock的Context和Application對象測試,也就是說你必須使用和應用程序實際運行的環境測試。

  • ActivityUnitTestCase :通常用來測試單獨的activity,在啟動被測試的activity之前,你可以Inject一個假的Context或是Application,使用這個mock的Context中一個隔離環境中運行被測試activity。通常用于activity的單元測試,而不和Android系統進行交互。

  • SingleLaunchActivityTestCase:用于測試單個activity,和ActivityUnitTestCase不同的是,它只運行setUp和tearDown一次,而不是在運行testCase中每個test Method前后運行setUp和tearDown,它可以保證運行多個測試之間fixture不會被重置,從而可以用來測試一些有關聯的方法。

說明:以上為Instrumentation單元測試框架的內容,通過配置InstrumentationTestRunner啟動器來執行自動化測試腳本,But在最新SDK中與InstrumentationTestRunner相關的所有類都被廢棄了,Google推薦使用最新的測試框架來編寫測試代碼。(AndroidJUnitRunner + Espresso + UIAutomater)

AndroidJUnitRunner

AndroidJUnitRunner是一個可以用來運行JUnit 3和JUnit 4樣式的測試類的Test Runner,并且同時支持Espresso和UI Automator。這是對于之前的InstrumentationTestRunner的一個升級,如果你去查看Gradle文檔中對于Testing配置的說明,會發現推薦的Test Runner為AndroidJUnitRunner。InstrumentationTestRunner只支持JUnit 3樣式的測試用例,而我們在寫Android測試用例時應該盡可能使用JUnit 4樣式來實現。

相對于Junit 3,JUnit 4有如下改進:

  • 測試類不需要再繼承junit.framework.TestCase類;

  • 測試方法名不再需要以test開頭;

  • 可以使用類似@Test,@Before,@After等注解來管理自己的測試方法;

  • 增加了一些Assert方法;

  • 支持對assert方法的static導入。

下面來看一個例子。如下的代碼段采用了JUnit 4風格進行編寫,并且調用了Espresso的API來進行了一些測試:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityInstrumentationTest {

    @Rule
    public ActivityTestRule mActivityRule = new ActivityTestRule<>(
    MainActivity.class);

    @Test
    public void sayHello(){
        onView(withText("Say hello!")).perform(click());
        onView(withId(R.id.textView)).check(matches(withText("Hello, World!")));
    }
}

從以上代碼可以看到,JUnit 4支持使用如下注解來管理整個測試用例:

  • @Before: 標識在運行測試方法之前運行的代碼。可以支持同一個Class中有多個@Before,但是這些方法的執行順序是隨機的。該注解替代了JUnit 3中的setUp()方法。

  • @After: 標識在運行測試方法結束之后運行的代碼。可以在其中做一些釋放資源的操作。該注解替代了JUnit 3中的tearDown()方法。

  • @Test: 標識一個測試方法。一個測試類中可以有多個測試方法,每個測試方法需要用一個@Test注解來標識。

  • @Rule: 簡單來說,是為各個測試方法提供一些支持。具體來說,比如我需要測試一個Activity,那么我可以在@Rule注解下面采用一個ActivityTestRule,該類提供了對相應Activity的功能測試的支持。該類可以在@Before和@Test標識的方法執行之前確保將Activity運行起來,并且在所有@Test和@After方法執行結束之后將Activity殺死。在整個測試期間,每個測試方法都可以直接對相應Activity進行修改和訪問。

  • @BeforeClass: 為測試類標識一個static方法,在測試之前只執行一次。

  • @AfterClass: 為測試類標識一個static方法,在所有測試方法結束之后只執行一次。

  • @Test(timeout=<milliseconds>): 為測試方法設定超時時間。

Espresso詳解

Espresso是一個新工具,相對于其他工具,API更加精確。并且規模更小、更簡潔并且容易學習。它最初是2013年GTAC大會上推出的,目標是讓開發者寫出更簡潔的針對APP的UI測試代碼。

優點:

  • 代碼快速上手
  • 容易擴展
  • 無需考慮復雜的多線程
  • 有Google做靠山

缺點:

  • 不支持跨應用的UI測試

Espresso 的主要組件:

onView(ViewMatchers).perform(ViewActions).check(ViewAssertions)

  • Espresso – 與視圖(views)交互的入口,并暴露了一些視圖(views)無關的API(例如回退按鈕)。
  • ViewMatchers – 實現匹配器的一組對象。允許可以通過多次的onView方法,在層次圖中找到目標視圖(views)。
  • ViewActions – 對視圖觸發動作(例如點擊)。
  • ViewAssertions – 用于插入測試關鍵點的一組斷言,可用于判斷某視圖(view)的狀態。

可以看出,與其他框架相比,Espresso代碼集成度更高,功能分塊更加集中:onView用于定位視圖,perform用于產生事件,check用于檢測checkpoint。

Espresso環境搭建

  1. 在Android Studio中新建一個Project;
  2. 修改Project中App/build.gradle腳本(Android studio2.2默認集成了Espresso) <br />

主要修改3處:

  • 在defaultConfig內增加
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
  • 增加packagingOptions,避免編譯時候Liscens的沖突;
  • 在dependencies中增加Espresso相關的引用;
defaultConfig {
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
packagingOptions {
    exclude 'LICENSE.txt' }
}

dependencies {
    testCompile 'junit:junit:4.12'
    androidTestCompile ('com.android.support.test:runner:0.5'){
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test:rules:0.5') {
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test.espresso:espresso-core:2.2.2'){
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test.espresso:espresso-idling-resource:2.2.2'){
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile ('com.android.support.test.espresso:espresso-intents:2.2.2'){
        exclude group: 'com.android.support',module: 'support-annotations'
    }
    androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.2.2') {
        exclude group: 'com.android.support',module: 'support-annotations'
        exclude group: 'com.android.support', module: 'appcompat'
        exclude group: 'com.android.support', module: 'appcompat-v7'
        exclude group: 'com.android.support', module: 'design'
        exclude group: 'com.android.support', module: 'support-v4'
        exclude module: 'recyclerview-v7'
    }
}

說明:

espresso-core:espresso的基礎庫

espresso-idling-resource:異步任務相關的庫

espresso-intents:提供對intent的支持庫

espresso-contrib:提供對特定組件(如:recycleView等)的支持庫

espresso-web:提供對webView測試的支持庫

Espresso API 鼓勵測試者以用戶會怎樣與應用交互的方式進行思考來定位 UI 元素并與它們交互。同時,框架不允許直接使用應用的活動和視圖,因為在非 UI 線程持有此類對象并對它們操作是造成測試花屏的主要原因。因此,你不會在 Espresso API 中看到諸如 getView 或 getCurrentActivity 等方法。但你仍然可以通過實現 ViewAction 和 ViewAssertion 來對視圖進行安全操作。

例如:

onView(withText("test")).check(matches(isDisplayed())).perform(click());

查找顯示文本為Test所對應的View顯示在界面上,并點擊該View

使用 onView 查找視圖

多數情況下,onView 方法使用 hamcrest 匹配器以期望在當前視圖結構里匹配一個(唯一的)視圖。該匹配器十分強大而且對用過 Mockito 或 JUnit 的人而言并不陌生。
想要查找的視圖一般會有唯一的 ?R.id? 值,使用簡單的 ?withId? 匹配器可以縮小搜索范圍。然而,當你在測試開發階段,無法確定 ?R.id值是合理的?。例如,指定的視圖可能沒有 R.id? 值或該值不唯一。這將使一般的 instrumentation 測試變得脆弱而復雜,因為通用的獲取視圖方式(通過 findViewById())已經不適用了。因此,你可能需要獲取持有視圖的私有對象 Activity 或 Fragment,或者找到一個已知其 ?R.id? 值的父容器,然后在其中定位到特定的視圖。

Espresso 處理該問題的方式很干脆,它允許你使用已存在的或自定義的 ViewMatcher 來限定視圖查找。
通過 ?R.id? 查找視圖:

onView(withId(R.id.my_view))

有時,?R.id?值會被多個視圖共享,此時你需要找到一個能唯一確定的屬性,你可以通過使用組合匹配器結合該屬性來縮小搜索范圍:

onView(allOf(withId(R.id.my_view), withText("Hello!")))

你也可以使用 ?not? 反轉匹配:

onView(allOf(withId(R.id.my_view), not(withText("Unwanted"))))

你可以在 ViewMatchers 類中查看 Espresso 提供的視圖匹配器。

注意:在一個良態的應用中,所有用戶可與之交互的視圖都應該包含說明文字或有一個內容描述。如果你不能通過使用 ‘withText’ 或 ‘withContentDescripiton’ 來縮小 onView 的搜索范圍,可以認為這是一個很明顯的 bug。
如果目標視圖在一個 ?AdapterView?(如 ?ListView?,?GridView?,?Spinner?)中,將不能使用 onView? 方法,推薦使用 ?onData? 方法。

在視圖上執行操作

當為目標視圖找到了合適的適配器后,你將可以通過 ?perform? 方法在該視圖上執行 ?ViewAction?。
例如,點擊該視圖:

onView(…).perform(click());

如果操作的視圖在 ?ScrollView?(水平或垂直方向)中,需要考慮在對該視圖執行操作(如 ?click()? 或 ?typeText()?)之前通過 ?scrollTo()? 方法使其處于顯示狀態。這樣就保證了視圖在執行其他操作之前是顯示著的。

onView(…).perform(scrollTo(), click());

注意:如果視圖已經是顯示狀態, ?scrollTo()? 將不會對界面有影響。因此,當視圖的可見性取決于屏幕的大小時(例如,同時在大屏和小屏上執行測試時),你可以安全的使用該方法。
你可以在 ViewActions 類中產看 Espresso 提供的視圖操作。

檢查一個視圖是否滿足斷言

斷言可以通過 ?check()? 方法應用在當前選中的視圖上。最常用的是 ?matches()? 斷言,它使用一個 ?ViewMatcher? 來判斷當前選中視圖的狀態。
例如,檢查一個視圖擁有 “Hello!”文本:

onView(…).check(matches(withText("Hello!")));

注意:不要將 “assertions” 作為 onView 的參數傳入,而要在檢查代碼塊中明確指定你檢查的內容,
例如:如果你想要斷言視圖的內容是 “Hello!” ,以下做法是錯誤的:

// Don't use assertions like withText inside onView.
onView(allOf(withId(...), withText("Hello!"))).check(matches(isDisplayed()));
//use it
onView(withId(...)).check(matches(withText("Hello!")));

從另一個角度講,如果你想要斷言一個包含 “Hello!” 文本的視圖是可見的(例如,在修改了該視圖的可見性標志之后),這段代碼是正確的。

注意:請留意斷言一個視圖沒有顯示和斷言一個視圖不在當前視圖結構之間的區別。

在 ?AdapterView? 控制器(ListView, GridView, ...)中使用 onData

AdapterView? 是一個從適配器中動態加載數據的特殊控件。最常見的 ?AdapterView? 是 ListView?。與像 ?LinearLayout? 這樣的靜態控件相反,在當前視圖結構中,可能只加載了 ?AdapterView? 子控件的一部分, 簡單的 ?onview()? 搜索不能找到當前沒有被加載的視圖。Espresso 通過提供單獨的 onData()? 切入點處理此問題,它可以在操作適配器中有該問題的條目或該條目的子項之前將其加載(使其獲取焦點)。

注意:
你可能不會對初始狀態就顯示在屏幕上的適配器條目執行 ?onData()? 加載操作,因為它們已經被加載了。然而,一直使用 ?onData()? 會更安全。

使用 onData 編寫一個簡單的測試

SimpleActivity? 包含一個 ?Spinner? ,該 Spinner? 中有幾個條目——代表咖啡類型的字符串。當選中其中一個條目時,?TextView? 內容會變成 ?“One %s a day!”?,其中 %s 代表選中的條目。此測試的目標是打開 ?Spinner?,選中一個條目然后驗證 ?TextView? 中包含該條目。由于 ?Spinner? 類基于 ?AdapterView?,建議使用 ?onData()? 而不是 ?onView()? 來匹配條目。

  1. 點擊 Spinner 打開條目選擇框
onView(withId(R.id.spinner_simple)).perform(click());
  1. 點擊 “Americano” 條目
    為了條目可供選擇,Spinner 用它的內容創建了一個 ?ListView?。該 ListView 可能會很長,而且它的元素不會出現在視圖結構中。通過使用 ?onData()? 我們強制將想要得到的元素加入到視圖結構中。Spinner 中的元素是字符串,我們想要匹配的條目是字符串類型并且值是 “Americano”。
onData(allOf(is(instanceOf(String.class)), is("Americano"))).perform(click());
  1. 驗證 TextView? 包含 “Americano” 字符串
onView(withId(R.id.spinnertext_simple).check(matches(withText(containsString("Americano"))));

自定義ListView匹配器事例:

public static Matcher<? super Object> withContactTitle(final String title) {
        return new BoundedMatcher(Contact.class) {

            @Override
            public void describeTo(Description description) {
                description.appendText("with id:"+title);
            }

            @Override
            protected boolean matchesSafely(Object item) {
                if(item instanceof Contact){
                    return title.equals(((Contact)item).getFullName());
                }
                return false;
            }
        };
    }

測試Menu菜單

分兩種情況:1、菜單按鈕顯示在title上,2、菜單隱藏在pop中

1、菜單顯示在titlebar中的測試方法:

onView(allOf(withId(R.id.action_create),withContentDescription("創建")))
        .check(matches(isDisplayed())).perform(click());
onView(withText("創建")).check(matches(isDisplayed()));
onView(allOf(withId(R.id.action_search),withContentDescription("搜索")))
        .check(matches(isDisplayed())).perform(click());
onView(withText("搜索")).check(matches(isDisplayed()));

說明:不論菜單在titleBar上顯示icon,還是顯示文字,菜單布局中都必須包含title,用作菜單描述,否則沒法定位對應的菜單項。
當菜單只顯示icon時,請使用withContentDescription(“title”)定位對應的菜單,withText(“title")不生效,原因是菜單中壓根沒有設置title
詳情查看ActionMenuItemView類

2、 菜單隱藏在pop中:
首先需要打開pop

//打開menu
openContextualActionModeOverflowMenu();
onView(allOf(withId(R.id.title),withText("創建"))).check(matches(isDisplayed())).perform(click());

注明:首先需要打開menu菜單框,title為菜單文本對應的TextView的id,此處固定為title,詳情請查看ListMenuItemView類

針對RecycleView測試方法

RecyclerView 是一個像 ListView、GridVIew 那樣呈現數據集合的 UI 組件,實際上它的目的是要替換掉這兩個組件。從測試的角度上來看我們感興趣的就是 RecyclerView 不是一個 AdapterView,這意味著你不能使用 onData() 去跟你的 list items 交互。

幸運的是,有一個叫 RecyclerViewActions 的類提供了簡單的 API 給我們操作 RecyclerView。RecyclerViewActions 是 espresso-contrib庫的一部分,這個庫的依賴可以在 build.gradle 中添加:

androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0');

這個時候就需要引用到一個 RecyclerViewActions ,RecyclerViewActions就是為了針對RecyclerView才出來的。 我們主要還是看看如何進行測試吧。

  • 點擊RecyclerView列表中第1個item
onView(withId(R.id.pull_refresh_list)).perform(RecyclerViewActions.actionOnItemAtPosition(1,click()));
  • 點擊帶有 “Effective Java ” 字符串的item
onView(withId(R.id.pull_refresh_list)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(taskName)),click()));

這里的hasDescendant 指代的是對應的item的后代中有包含對應文本的內容的。不過使用這個需要小心 因為很有可能會出現兩個同樣內容的。

  • actionOnHolderItem 的用法
@Test
public void testItemSelect() {
    onView(withId(R.id.pull_refresh_list))
            .perform(RecyclerViewActions.actionOnHolderItem(
                    new CustomViewHolderMatcher(hasDescendant(withText("Effective Java "))), click()));

}

private static class CustomViewHolderMatcher extends TypeSafeMatcher<RecyclerView.ViewHolder> {
    private Matcher<View> itemMatcher = any(View.class);

    public CustomViewHolderMatcher() { }

    public CustomViewHolderMatcher(Matcher<View> itemMatcher) {
        this.itemMatcher = itemMatcher;
    }

    @Override
    public boolean matchesSafely(RecyclerView.ViewHolder viewHolder) {
        return TaskListAdapter.ViewHolder.class.isAssignableFrom(viewHolder.getClass())
                && itemMatcher.matches(viewHolder.itemView);
    }

    @Override
    public void describeTo(Description description) {
        description.appendText("is assignable from CustomViewHolder");
    }
}

<br />

Espress線程同步問題

使用 registerIdlingResource 與自定義資源同步

Espresso 的核心是它可以與待測應用無縫同步測試操作的能力。默認情況下,Espresso 會等待當前消息隊列中的 UI 事件執行(默認是 AsyncTask)完畢再進行下一個測試操作。這應該能解決大部分應用與測試同步的問題。

然而,應用中有一些執行后臺操作的對象(比如與網絡服務交互)通過非標準方式實現;例如:直接創建和管理線程,以及使用自定義服務。

此種情況,我們建議你首先提出可測試性的概念,然后詢問使用非標準后臺操作是否必要。某些情況下,可能是由于對 Android 理解太少造成的,并且應用也會受益于重構(例如,將自定義創建的線程改為 AsyncTask)。然而,某些時候重構并不現實。慶幸的是 Espresso 仍然可以同步測試操作與你的自定義資源。

以下是我們需要完成的:

  • 實現 ?IdlingResource? 接口并暴露給測試。
  • 通過在 setUp 中調用 ?Espresso.registerIdlingResource? 注冊一個或多個 IdlingResource 給 Espresso。

需要注意的是 IdlingResource 接口是在待測應用中實現的,所以你需要謹慎的添加依賴:

// IdlingResource is used in the app under test
compile 'com.android.support.test.espresso:espresso-idling-resource:2.2.2'

例如:項目使用OkHttp庫,此時需要定制針對OKHttp的同步測試,代碼如下:

public final class OkHttp3IdlingResource implements IdlingResource {
    /**
     * Create a new {@link IdlingResource} from {@code client} as {@code name}. You must register
     * this instance using {@code Espresso.registerIdlingResources}.
     */
    @CheckResult @NonNull
    @SuppressWarnings("ConstantConditions") // Extra guards as a library.
    public static OkHttp3IdlingResource create(@NonNull String name, @NonNull OkHttpClient client) {
        if (name == null) throw new NullPointerException("name == null");
        if (client == null) throw new NullPointerException("client == null");
        return new OkHttp3IdlingResource(name, client.dispatcher());
    }

    private final String name;
    private final Dispatcher dispatcher;
    volatile ResourceCallback callback;

    private OkHttp3IdlingResource(String name, Dispatcher dispatcher) {
        this.name = name;
        this.dispatcher = dispatcher;
        dispatcher.setIdleCallback(new Runnable() {
            @Override
            public void run() {
                ResourceCallback callback = OkHttp3IdlingResource.this.callback;
                if (callback != null) {
                    callback.onTransitionToIdle();
                }
            }
        });
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public boolean isIdleNow() {
        return dispatcher.runningCallsCount() == 0;
    }

    @Override
    public void registerIdleTransitionCallback(ResourceCallback callback) {
        this.callback = callback;
    }
}

//測試代碼中加入以下代碼塊
@Before
public void registerIdlingResource(){
    okHttp3IdlingResource = OkHttp3IdlingResource.create("okhttp",HaizhiRestClient.getHttpClient());
    Espresso.registerIdlingResources(okHttp3IdlingResource);
       
}
@After
public void unregisterIdlingResource(){
    Espresso.unregisterIdlingResources(okHttp3IdlingResource);
}

Espresso-Intents

Espresso-Intents 是 Espresso 的一個擴展,它使驗證和存根待測應用向外發出的意圖成為可能。它類似于 Mockito,但是針對的是 Android 的意圖(專門針對Android的intent的擴展)。
Espresso-Intents 只兼容 Espresso 2.1+ 和 testing support library 0.3

在應用的build.gradle文件中添加以下配置

androidTestCompile ('com.android.support.test.espresso:espresso-intents:2.2.2'){
    exclude group: 'com.android.support', module: 'support-annotations'
}

IntentsTestRule

使用 Espresso-Intents 時,應當用 ?IntentsTestRule? 替換 ?ActivityTestRule?。IntentsTestRule? 使得在 UI 功能測試中使用 Espresso-Intents API 變得簡單。該類是 ?ActivityTestRule? 的擴展,它會在每一個被 ?@Test? 注解的測試執行前初始化 Espresso-Intents,然后在測試執行完后釋放 Espresso-Intents。被啟動的 activity 會在每個測試執行完后被終止掉,此規則也適用于 ?ActivityTestRule?。

驗證意圖(Intent validation)

Espresso-Intents 會記錄待測應用里所有嘗試啟動 Activity 意圖。使用 intended API(與 ?Mockito.verify? 類似)你可以斷言特定的意圖是否被發出。

驗證外發意圖的簡單示例:

onView(withText("send")).perform(click());
//驗證發送短信界面成功調用
Uri smsToUri = Uri.parse("smsto:10086");
intended(hasData(smsToUri));

意圖存根(Intent stubbing)

使用 intending API(與 ?Mockito.when? 類似)你可以為通過 startActivityForResult 啟動的 Activity 提供一個響應結果(尤其是外部的 Activity,因為我們不能操作外部 activity 的用戶界面,也不能控制 ?ActivityResult? 返回給待測 Activity)。

使用意圖存根的示例:

@Test
public void startSecondActivity(){
     Intent intent = new Intent();
     intent.putExtra("test","test");
     Instrumentation.ActivityResult result =
                new Instrumentation.ActivityResult(Activity.RESULT_OK,intent);
//        intending(anyIntent()).respondWith(result);
        //必須使用完整類名(包名+類名)
    intending(hasComponent(InstrumentationRegistry.getTargetContext().getPackageName()+"."+
                SecondActivity.class.getSimpleName())).respondWith(result);
        onView(withText("start")).perform(click());
        onView(withText("test")).check(matches(isDisplayed()));
}

說明:從MainActivity中點擊按鈕start跳轉到SecondActivity中,返回時帶回參數Test 顯示到MainActivity的界面中。

意圖匹配器(Intent Matchers)
intending? 和 ?intended? 方法用一個 hamcrest ?Matcher<Intent>? 作為參數。 Hamcrest 是匹配器對象(也稱為約束或斷言)庫。有以下選項:

  • 使用現有的意圖匹配器:最簡單的選擇,絕大多數情況的首選。
  • 自己實現意圖匹配器,最靈活的選擇(參考 Hamcrest 教程 的 “Writing custom matchers” 章節)

以下是一個使用現有的意圖匹配器驗證意圖的示例:

intended(allOf(
    hasAction(equalTo(Intent.ACTION_VIEW)),
    hasCategories(hasItem(equalTo(Intent.CATEGORY_BROWSABLE))),
    hasData(hasHost(equalTo("www.google.com"))),
    hasExtras(allOf(
        hasEntry(equalTo("key1"), equalTo("value1")),
        hasEntry(equalTo("key2"), equalTo("value2")))),
        toPackage("com.android.browser")));

Mock

mock的概念其實很簡單,所謂的mock就是創建一個類的虛擬對象,在測試環境中用來替換掉真是的對象,以達到兩個目的:

  • 驗證這個對象的某些方法的調用情況,調用了多少次,參數是什么等等。
  • 指定這個對象的某些方法的行為,返回特定的值,或者是執行特定的動作。

Mockito

要是用mock一般需要使用到mock框架,Mockito框架是java界使用最廣泛的一個mock框架。

1、申明依賴

testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.+'
// 如果你要使用Mockito 用于 Android instrumentation tests,那么需要你添加以下三條依賴庫
androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"

2、創建Mock對象

在 Mockito 中你可以使用 mock() 方法來創建一個模擬對象,也可以使用注解的方式 @Mock 來創建 ,這里推薦使用注解。需要注意的是,如果是使用注解方式,需要在使用前進行初始化。

使用注解方式創建有三種初始化方式:

1)、使用 MockitoAnnotations.initMocks(this) 方式

public class MockitoAnnotationsTest {
    @Mock
    AccountData accountData;
    @Before
    public void setupAccountData(){
        MockitoAnnotations.initMocks(this);
    }
    @Test
    public void testIsNotNull(){
        assertNotNull(accountData);
    }
}

2)、使用 @RunWith(MockitoJUnitRunner.class) 方式

@RunWith(MockitoJUnitRunner.class)
public class MockitoJUnitRunnerTest {
    @Mock
    AccountData accountData;

    @Test
    public void testIsNotNull() {
        assertNotNull(accountData);
    }
}

3)、使用 MockitoRule 方式

public class MockitoRuleTest {
    @Mock
    AccountData accountData;
    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();

    @Test
    public void testIsNotNull(){
        assertNotNull(accountData);
    }
}

使用mock方式創建mock對象

public class MockitoTest {

    AccountData accountData;
    @Before
    public void setup(){
       accountData = Mockito.mock(AccountData.class);
    }
    @Test
    public void testIsNotNull(){
        assertNotNull(accountData);
    }
}

說明:

  • Mockito.mock() 并不是mock一整個類,而是根據傳進去的一個類,mock出屬于這個類的一個對象,并且返回這個 mock對象;而傳進去的這個類本身并沒有改變,用這個類new出來的對象也沒有受到任何改變!
  • Mockito.verify() 的參數必須是mock對象,也就是說,Mockito只能驗證mock對象的方法調用情況

Mockito的使用

1、驗證方法的調用

前面我們講了驗證一個對象的某個method得到調用的方法:

Mockito.verify(accountData).isLogin();

這行代碼驗證的是, accountData 的 isLogin() 方法得到了 一次 調用。因為這行代碼其實是:

Mockito.verify(accountData, Mockito.times(1)).isLogin();

因此,如果你想驗證一個對象的某個方法得到了多次調用,只需要將次數傳給 Mockito.times() 就好了。

Mockito.verify(accountData, Mockito.times(3)).isLogin(); //accountData的isLogin方法調用了3次。

對于調用次數的驗證,除了可以驗證固定的多少次,還可以驗證最多,最少從來沒有等等,方法分別是:

  • Mockito.verify() : 驗證Mock對象的方法是否被調用。
  • Mockito.times() : 調用mock對象的次數
  • Mockito.atMost(count) , Mockito.atLeast(count) , Mockito.never() :最多次數,最少次數,永遠調用。
  • Mockito.anyInt() , Mockito.anyLong() , Mockito.anyDouble()等等 : 參數設置-任意的Int類型,任意的Long類型。。。等。
  • anyCollection,anyCollectionOf(clazz), anyList(Map, set), anyListOf(clazz)

2、指定mock對象的某些方法的行為

到目前為止,我們介紹了mock的一大作用:驗證方法調用。我們說mock主要有兩大作用,第二個大作用是:指定某個方法的返回值,或者是執行特定的動作。
那么接下來,我們就來介紹mock的第二大作用,先介紹其中的第一點:指定mock對象的某個方法返回特定的值。

//希望 isLogin() 方法被調用時返回true,那么你可以這樣寫:

when(accountData.isLogin()).thenReturn(true);
//驗證結果
boolean islogin = accountData.isLogin();
assertTrue(islogin);

//如果你希望 getUserName() 被調用返回Jack
when(accountData.getUserName()).thenReturn("Jack");
assertEquals("Jack",accountData.getUserName());

//如果你希望對 setUserName(String userName) 方法中參數進行測試
accountData.setUserName("haha");
verify(accountData).setUserName(Matchers.eq("haha"));

同樣的,你可以用 any 系列方法來指定"無論傳入任何參數值,都返回xxx":

//當調用accountData的setUserName1方法時,返回haha,無論參數是什么
when(accountData.setUserName1(anyString())).thenReturn("haha");

在這里,我們想進一步測試傳給 accountData.login() 的 NetworkCallback 里面的代碼,驗證view得到了更新等等。在測試環境下,我們并不想依賴 accountData.login() 的真實邏輯,而是讓 accountData.login 直接調用傳入的 NetworkCallback 的 onSuccess 或 onFailure 方法。這種指定mock對象執行特定的動作的寫法如下:

Mockito.doAnswer(desiredAnswer).when(mockObject).targetMethod(args);

傳給 doAnswer() 的是一個 Answer 對象,我們想要執行什么樣的動作,就在這里面實現。結合上面的

@Test
public void test_login(){
    activityTestRule.getActivity().setAccountData(accountData);
    doAnswer(new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {

            //這里可以獲取傳給login的參數
            Object[] args = invocation.getArguments();

            //callback是第三個參數
            NetWorkCallBack callBack = (NetWorkCallBack) args[2];

            callBack.onFailure(500,"Server error");
            return 500;
        }
    }).when(accountData).login(anyString(),anyString(),any(NetWorkCallBack.class));

    onView(withText("button")).perform(click());
    onView(withText("code=500:errorMsg=Server error")).check(matches(isDisplayed()));
}

Spy

前面我們講了mock對象的兩大功能,對于第二大功能: 指定方法的特定行為,不知道你會不會好奇,如果我不指定的話,它會怎么樣呢?那么現在補充一下,如果不指定的話,一個mock對象的所有非void方法都將返回默認值:int、long類型方法將返回0,boolean方法將返回false,對象方法將返回null等等;而void方法將什么都不做。然而很多時候,你希望達到這樣的效果:除非指定,否者調用這個對象的默認實現,同時又能擁有驗證方法調用的功能。這正好是spy對象所能實現的效果。創建一個spy對象,以及spy對象的用法介紹如下:

//假設目標類的實現是這樣的
public class PasswordValidator {
    public boolean verifyPassword(String password) {
        return "test_spy".equals(password);
    }
}

@RunWith(MockitoJUnitRunner.class)
public class SpyTest {
    @Spy
    PasswordValidator passwordValidator;

    @Test
    public void test_verifyPassword(){
        //跟創建mock類似,只不過調用的是spy方法,而不是mock方法。spy的用法
        Assert.assertTrue(passwordValidator.verifyPassword("test_spy"));
        Assert.assertFalse(passwordValidator.verifyPassword("test_spy1"));

        //spy對象的方法也可以指定特定的行為
        when(passwordValidator.verifyPassword(anyString())).thenReturn(true);

        Assert.assertTrue(passwordValidator.verifyPassword("test_spy12"));
        //同樣的,可以驗證spy對象的方法調用情況
        verify(passwordValidator).verifyPassword("test_spy12");
    }
}

總之,spy與mock的唯一區別就是默認行為不一樣:spy對象的方法默認調用真實的邏輯,mock對象的方法默認什么都不做,或直接返回默認值。

Android真機使用Mockito-1.10.19+Dexmaker-1.2在Mock繼承抽象父類的子類時報告錯誤“java.lang.AbstractMethodError: abstract method not implemented”

在項目的 build.gradle中的聲明如下:

androidTestCompile 'org.mockito:mockito-core:1.+'
androidTestCompile "com.google.dexmaker:dexmaker:1.2"
androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2"

這個問題只在 Dalvik虛擬機下面發生異常,相同的代碼在 ART下面是完全正常的。
導致問題發生的原因是 Google提供的 dexmaker庫存在 BUG導致的,而這個庫,從 Maven Center上看,自從 2012年開始就沒有提供過任何的更新了。
解決方法是不使用 Google提供的 dexmaker,而是使用com.crittercism.dexmaker修正過這個 BUG的版本。

androidTestCompile 'org.mockito:mockito-core:1.10.19'
androidTestCompile 'com.crittercism.dexmaker:dexmaker:1.4'
androidTestCompile "com.crittercism.dexmaker:dexmaker-dx:1.4"
androidTestCompile 'com.crittercism.dexmaker:dexmaker-mockito:1.4'

參考資料

官方文檔
Espresso 自動化測試框架介紹
測試與基本規范
使用MVP+Dagger2+Espresso+Mockito構建的項目樣板

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

推薦閱讀更多精彩內容