Android單元測試介紹

Android 測試支持庫介紹

Android的測試支持庫爲測試Android應用提供了大量框架。該庫提供了一組API快速構建和運行測試代碼,包括JUnit4和功能用戶界面(UI)測試。能夠從Android Studio IDE中或命令行這執行。html

Android的測試支持庫可經過Android SDK管理器獲取。java

測試支持庫特性

AndroidJUnitRunner:兼容JUnit 4測試運行器。 Espresso:UI測試框架;適合在單個應用的功能UI測試。 UI Automator:UI測試框架;適用於跨應用的功能UI測試及安裝應用。python

AndroidJUnitRunner

AndroidJUnitRunner類是JUnit測試運行器,可讓你在Android設備上執行JUnit3或JUnit4中風格的測試類,兼容Espresso和UI Automator測試框架。測試運行器加載測試包和應用,運行測試並報告測試結果。該類取代 InstrumentationTestRunner類(僅支持JUnit 3)。android

這個運行器的主要特色:git

  • JUnit支持github

  • 得到Instrumentation信息正則表達式

  • 測試篩選shell

  • 測試分片數據庫

要求的Android2.2(API 8)或更高。express

 

AndroidJunitRunner經過InstrumentationRegistry提供了訪問instrumentation的API。

  • InstrumentationRegistry.getInstrumentation()返回當前正在運行的Instrumentation

  • InstrumentationRegistry.getContext()返回此Instrumentation軟件包的上下文。

  • InstrumentationRegistry.getTargetContext()返回目標應用的應用上下文。

  • InstrumentationRegistry.getArguments()返回傳遞給此Instrumentation的參數Bundle。當你要訪問命令行參數時很是有用。

測試運行器兼容JUnit3和JUnit4的(最高JUnit4.10)測試。在同一個包混淆JUnit 3和和JUnit4測試代碼可能會致使不可預測的結果。instrumented JUnit 4測試類在設備或仿真器上運行,必須在前面加上@RunWith(AndroidJUnit4.class)註釋。

 

好比測試CalculatorActivity類中的加操做:

 

import android.support.test.runner.AndroidJUnit4;
import android.support.test.runner.AndroidJUnitRunner;
import android.test.ActivityInstrumentationTestCase2;

@RunWith(AndroidJUnit4.class)
public class CalculatorInstrumentationTest
        extends ActivityInstrumentationTestCase2<CalculatorActivity> {
        
    @Before
    public void setUp() throws Exception {
        super.setUp();
        
        // Injecting the Instrumentation instance is required
        // for your test to run with AndroidJUnitRunner.
        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
        mActivity = getActivity();
    }
    
    @Test
    public void typeOperandsAndPerformAddOperation() {
        // Call the CalculatorActivity add() method and pass in some operand values, then
        // check that the expected value is returned.
    }
    
    @After
    public void tearDown() throws Exception {
        super.tearDown();
    }
}

InstrumentationRegistry類能夠訪問測試運行的信息。該類包括Instrumentation對象,目標應用上下文對象,測試應用上下文對象及傳遞到測試的命令行參數。

JUnit 4.x的測試可使用annotation來配置測試運行,並支持Androidannotation:

@RequiresDevice:物理設備上運行。

@SdkSupress:限定最低SDK版本。例如@SDKSupress(minSdkVersion=18)
@SmallTest,@MediumTest和@LargeTest:測試分級。

單個test suite能夠分片,同一Instrumentation的同一分片能夠做爲一個組。每一個片都有索引號。當運行測試,使用-e numShards選項指定片數和-e shardIndex選項來指定要運行的片。

例如分紅10個碎片,僅執行第二片測試,請使用如下命令:

adb shell am instrument -w -e numShards 10 -e shardIndex 2

Espresso

Espresso提供了一組API來構建UI測試來測試用戶流程。這些API讓你寫簡潔和可靠運行的自動化UI測試。Espresso很是適合白盒自動測試,測試代碼利用了被測應用的代碼細節。

Espresso的主要特性:

  • 視圖匹配(View matching): 靈活的API用於查看和適配目標應用。

  • Action API:一套擴展的action API自動化UI交互。

  • UI線程同步(UI thread synchronization)以提升測試的可靠性。

要求Android2.2(API 8)或更高。

Espresso.onView()方法能夠訪問目標應用程序的UI組件並與之交互。該方法接受一個Matcher參數,搜索視圖層來定位視圖實例。定位方法能夠基於類名、內容描述、R.id、顯示的文本。好比

onView(withId(R.id.my_button));


若是搜索成功,onView()方法返回對應view的引用,可執行用戶操做和斷言。

AdapterView由子view動態生成。若是目標視圖在AdapterView(ListView或GridView)中,onView()方法可能不起做用,由於可能只加載了一部分,Espresso.onData()則能夠。

ViewActions能夠執行視圖點擊(View clicks),滑動(Swipes),按鍵或者按鈕(Key and button press)、文本輸入(Typing text)、打開連接(Opening a link)。

// Type text into an EditText view, then close the soft keyboard
onView(withId(R.id.editTextUserInput))
    .perform(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
// Press the button to submit the text change
onView(withId(R.id.changeTextBt)).perform(click());

因爲時間問題,在Android設備上測試隨機失敗。以前通常經過sleep和超時處理解決。Espresso測試框架處理Instrumentation和UI線程之間的同步,很好地解決了這些問題。

API參考:developer.android.com/reference/android/support/test/package-summary.html
測試參考:http://developer.android.com/training/testing/ui-testing/espresso-testing.html

 

UI Automator

UI Automator提供了一組API來構建基於交互UI的測試。API容許你執行操做,如打開設置菜單,很是適合黑盒自動化測試,測試代碼不依賴於應用的內部實現。

主要特性:

  • UI Automator Viewer:檢查的佈局層次。

  • API來獲取設備狀態信息並執行操做。

  • API跨應用測試。

要求Android4.3(API等級18)或者更高。

uiautomatorviewer提供了一個方便的圖形用戶界面進行掃描和分析在Android設備上當前顯示的UI組件。您可使用此工具來檢查的佈局層次和查看UI組件。

UiDevice 類能夠訪問設備並進行操做。你能夠調用它的方法來訪問設備屬性,如當前的方向或顯示尺寸。該UiDevice類也讓您執行操做,例如:旋轉設備;按下D- pad按鈕;按Back、Home、Menu等;打開通知樹欄;當前窗口截圖等。好比按Home鍵:UiDevice.pressHome()。

更 多應用相關的API: UiCollection枚舉容器的UI元素以計數,或經過文字(或屬性等)定位子元素; UIObject表示是在設備上可見的UI元素; UiScrollable:爲可滾動UI容器提供查找支持; UiSelector:查詢一個或者多個UI元素; Configurator: 設置參數。好比:

// Initialize UiDevice instance
mDevice = UiDevice.getInstance(getInstrumentation());
// Perform a short press on the HOME button
mDevice().pressHome();
// Bring up the default launcher by searching for
// a UI component that matches the content-description for the launcher button
UiObject allAppsButton = mDevice
        .findObject(new UiSelector().description("Apps"));
// Perform a click on the button to bring up the launcher
allAppsButton.clickAndWaitForNewWindow();

API參考:http://developer.android.com/reference/android/support/test/package-summary.html
實例:http://developer.android.com/training/testing/ui-testing/uiautomator-testing.html

https://pypi.python.org/pypi/uiautomator 用python封裝了uiautomator,操做起來更簡單,推薦使用。

 

測試支持庫安裝

android {
    defaultConfig {
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

 

  • 啓動Android SDK管理器。

  • 在SDK管理器窗口,滾動到軟件包列表的末尾,找到其餘文件夾,若有必要,展開以顯示其內容。

  • 選擇 Android Support Repository。

  • 點擊Install packages。

    下載後安裝支持庫文件到您現有的Android SDK目錄。該庫文件位於你的SDK的子目錄:<sdk>/extras/android/m2repository。

    Android的測試支持庫位於android.support.test包。

    Gradle中使用:build.gradle文件中添加這些依賴關係:


    dependencies {
      androidTestCompile 'com.android.support.test:runner:0.4'
      // Set this dependency to use JUnit 4 rules
      androidTestCompile 'com.android.support.test:rules:0.4'
      // Set this dependency to build and run Espresso tests
      androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
      // Set this dependency to build and run UI Automator tests
      androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
    }



    設置AndroidJUnitRunner爲默認測試:
    強烈建議您一塊兒使用Android測試支持庫與Android Studio IDE。 Android的Studio支持測試開發,如:

        基於Gradle的靈活的構建系統,支持測試代碼的依賴管理。
        單個項目包含應用程序源代碼和測試代碼。
        支持虛擬或物理設備的部署和運行測試,支持命令行或圖形用戶界面

    更多介紹參見:http://developer.android.com/sdk/index.html

參考資料

  • Testing Support Library

  • http://wiki.jikexueyuan.com/project/android-weekly/issue-145/android-ui-auto-testing.html

  • http://developer.android.com/intl/zh-cn/training/testing/ui-testing/espresso-testing.html

  • http://www.stevenmarkford.com/android-ui-testing-with-espresso-basics-tutorial/

  • http://www.csdn.net/article/2015-08-19/2825493-using-espresso-for-easy-ui-testing

 

做者博客:http://my.oschina.net/u/1433482/ python測試開發精華羣 291184506 PythonJava單元白盒測試 144081101

Android測試基礎

android測試框架(Android Testing Framework)是開發環境的一部分,它提供了架構和強大的工具幫助你從單元到框架測試應用的各個方面。

本部分有點偏舊,若是和其餘部份內容有衝突,以其餘內容爲準。


關鍵特性:

  • 基 於 JUnit,可直接使用JUnit測試一些與Android AP不相關的類,或使用 Android的JUint 擴展來測試 Android 組件。若是你剛開始接觸 Android 測試,能夠先從 AndroidTestCase開始寫一些通用目的的測試用例,而後再寫較複雜的測試用例。

  • Android JUint擴展提供了對 Android 特定組件的測試類,提供了建立mock對象輔助方法及控制組件生命週期的方法。

  • Test suite包含在測試包中,操做和主程序包相似。

  • Eclipse ADT包含了建立和測試的SDK 工具,並可經過命令行在其它IDE使用。這些工具從被測試的應用讀取信息並自動建立測試包的build 文件,mainfest文件和文件目錄結構。

  • SDK 提供了moneyrunner(用python書寫腳本)及Monkey(發送僞隨機事件的UI壓力測試工具)。


本文檔描述了android測試框架的基礎,包含測試結構、開發測試的API以及啓動測試和查看測試結果的工具。本文假設你有android應用編程以及JUnit測試的基礎。

測試框架圖以下:

測試結構

android的構建和測試工具假設測試項目都組織成相似測試、測試類、測試包和測試項目的標準結構。

android測試基於JUnit。一般JUnit測試就是測試「待測試應用」的方法。測試方法構成的類爲test cases (或者test suites)。

JUnit 中編譯測試源文件到class文件中。相似地android中用android的編譯工具編譯測試包中的測試源文件爲class文件。在JUnit 中test runner來執行測試類。在android中使用測試工具加載測試包和被測應用,而後調用android的test runner。

 

測試項目

測試項目就是一個目錄或者eclipse 工程,可在其中新建源代碼、manifest文件以及測試包的其它文件。建議使用Android tool建立測試項目:

  • 自動配置InstrumentationTestRunner爲測試執行器。必須使用InstrumentationTestRunner或者其子類來執行JUnit測試。

  • 爲測試包取合適的名字。若是被測應用爲com.mydomain.myapp,Android tool自動命名測試包名爲com.mydomain.myapp.test。

  • 自動建立合適的構建文件、manifest 文件以及目錄結構。


推薦的目錄結構:


  MyProject/
      AndroidManifest.xml
      res/
          ... (resources for main application)
      src/
          ... (source code for main application) ...
      tests/
          AndroidManifest.xml
          res/
              ... (resources for tests)
          src/
              ... (source code for tests)

注:這個在實際操做中每每根據IDE而不一樣,請以具體IDE的實例爲準。 


測試API

Android測試API基於JUnit API 並擴展了instrumentation框架和Android特有的測試類。

JUnit


使用JUnit的TestCase類可對未使用android API的類進行單元測試。TestCase也是AndroidTestCase(測試依賴於android的對象)的基類。 AndroidTestCase還提供了android特有的 setup、teardown以及其它幫助方法。

JUnit的Assert類能夠顯示結果,assert方法比較指望值與實際結果,在比較失敗時拋出異常。android一樣提供了擴展了比較類型的斷言類,以及用來測試UI的斷言類。

注意android測試API支持JUnit 3的代碼風格,不支持JUnit 4。

Instrumentation

android instrumentation是android系統中一系列控制方法或鉤子(hooks)。這些鉤子能夠的正常生命週期內獨立地控制組件。它一樣能夠控制android如何加載應用。

一般android組件會按照系統指定的生命週期來運行,例如Activity的生命週期開始於它被Intent激活,其onCreate()方法會被調 用,接下來是onResume(),當用戶啓動另一個應用,onPause()方法會調用 ,若是Activity調用finish()方法,它的onDestroy()方法也會被調用。android framework API不會提供方法讓你在代碼中直接調用這些回調方法,可是用instrumentation能夠。

系統把應用中的全部組件運行在同一個進程中,你可讓一些組件,好比content provider,運行在單獨的進程中。可是沒法強制應用與另外其餘正在運行的應用運行在同一個進程中。

經過android imstrumentation,你能夠在測試代碼中直接調用回調方法,讓你滲透組件的生命週期,就像調試。下面的測試代碼演示瞭如何用instrumentation來測試Activity保存和恢復狀態:
  

    // Start the main activity of the application under test
    mActivity = getActivity();
    
    // Get a handle to the Activity object's main UI widget, a Spinner
    mSpinner = (Spinner)mActivity.findViewById(com.android.example.spinner.R.id.Spinner01);
    
    // Set the Spinner to a known position
    mActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION);
    
    // Stop the activity - The onDestroy() method should save the state of the Spinner
    mActivity.finish();
    
    // Re-start the Activity - the onResume() method should restore the state of the Spinner
    mActivity = getActivity();
    
    // Get the Spinner's current position
    int currentPosition = mActivity.getSpinnerPosition();
    
    // Assert that the current position is the same as the starting position
    assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition);

代碼中使用的關鍵方法是getActivity(),它屬於android instrumentation API。調用該方法纔會啓動Activity。你能夠提早配置測試所需的環境(test fixture)。

Instrumentation能夠加載測試包和被測試應用到同一個進程,這樣測試代碼能夠調用應用組件的方法,修改和檢查組件中的域。

Test case 類


android提供了幾個繼承自TestCase和Assert的test case 類,它們都有andorid 特有的setup、teardown以及其它的輔助方法。


AndroidTestCase


通用的test case類,繼承了TestCase和Assert類。它提供了標準的JUnit中的setup()和teardown()方法,同時還有JUnit的Assert方法。另外它也提供了用來測試權限的方法以及經過清除必定的類引用來防止內存泄露的方法。

組件特有的test case

android測試框架的一個重要特色是組件test case類。它們有獨特的setup和teardown及控制組件生命週期。同時它們也提供mock方法。

一、Activity Testing

二、Content Provider Testing

三、Service Testing

Android並無爲 BroadcastReceiver提供單獨的test case 類。能夠經過測試發送Intent對象給它的組件來測試BroadcastReceiver,檢查BroadcastReceiver回覆是否正確。

ApplicationTestCase


用 ApplicationTestCase測試Application對象的setup和teardown。這些對象維護着應用程序包中全部組件信息的全局 狀態,該test case 用於驗證manifest 文件中的<application>元素是否正確配置。然而記住這個test case沒法控制應用包組件的測試。

InstrumentationTestCase

若是想在test case 類中使用 instrumentation 的方法,必須使用InstrumentationTestCase或者它的子類。Activity test case 繼承該基類。

Assertion 類

Android test case 類繼承自JUnit,能夠用斷言來顯示測試結果。assertion方法將測試返回的真實值和指望值進行比較,若是比較失敗它會拋出AssertionException。用Assertion比打印log更方便,有更好的測試性能。

除了JUnit的Assert類的方法,測試API同時也提供了MoreAsserts和ViewAsserts類:

  • MoreAsserts包含更多強大的斷言,例如進行正則表達式匹配的assertContainsRegex(String, String)。

  • ViewAsserts包含不少關於View的斷言,例如它包含用來測試View是否在屏幕的特定的(x, y)位置的assertHasScreenCoordinates(View, View, int, int)方法,這些斷言簡化了UI中的幾何和對準測試。

模擬對象類


爲了解決測試過程當中的依賴,Android提供了建立模擬系統對象的類,好比Context對象、 ContentProvider對象、ContentResolver對象以及Service對象。有些test case能模擬的Intent對象。經過使用這些模擬對象,你能夠將測試與系統的其他部分隔離開,同時也知足了測試中的依賴,這些類都在包 android.test和android.test.mock中。

模擬對象經過打樁或者重載正常操做來實現將測試與正在運行的系統隔 離。例如MockContentResolver對象用它自有的與系統隔離的本地框架來代替一般的resolver框架。同時 MockContentResolver不使用notifyChange(Uri, ContentObserver, boolean)方法,這樣測試環境之外的observer對象不會被意外觸發。

模擬對象類也經過提供正常類的子類來知足測試的依賴,該子類除了你覆寫的方法外其它都是不起做用的。例如,MockResources對象是Resources類的子類,每一個方法在調用時都會拋出異常。要使用它,你只須要重載須要的方法。

下面是Android中可用的模擬對象類:

基本的模擬對象類

MockApplication、 MockContext、MockContentProvider、MockCursor,、MockDialogInterface、 MockPackageManager和MockResources提供了簡單有用的模擬策略(打樁),在調用時都會拋出 UnsupportedOperationException異常。使用它,你只須要重載須要的方法。

注意:MockContentProvider和MockCursor是API Level 8 中新加入。

Resolver 模擬對象

MockContentResolver 經過屏蔽系統正常的resolver框架來爲content provider 提供隔離的測試。MockContentResolver不是在系統中查找提供authority的content provider,而是使用本身的內部表,你必須顯式地用addProvider(String, ContentProvider)方法將provider添加到表中。

經過這個特性能夠將模擬的content provider與authority關聯,新建provider對象但使用測試數據,你甚至能夠設置provider的authority爲null。 實際上MockContentResolver對象將你的測試與包含真實數據的provider隔離。你能夠控制provider的功能並防止測試影響真 實數據。


Context測試  


android提供了兩個Context類來提供測試:

  • IsolatedContext類提供隔離的Context,使用該Context的文件、目錄和數據庫操做都會在測試區域。儘管功能有限,但足以應對系統調用,容許在不影響當前設備上的真實數據的前提下測試應用的數據操做。

  • RenamingDelegatingContext 的Context的大部分功能都由現有的Context來處理,可是文件和數據庫操做都由IsolatedContext來處理,隔離的部分使用一個測試 目錄,而且建立特殊的文件和目錄名,你能夠本身控制命名,也可讓constructor自動指定。該類爲進行數據操做建立隔離區域提供了快捷辦法,同時 不會影響Context其它的正常操做。

運行測試

test case由test runner類運行,test runner加載test case類、初始化、運行及清理測試。android test runner必須配置,這樣啓動應用的系統工具能夠控制測試包如何加載test case和被測試應用。通常在manifest文件中設定。

InstrumentationTestRunner是android中主要的test runner類,它擴展了JUnit test runner框架而且是已配置,可以執行任何android系統提供的test case 類而且支持全部類型的測試。

你 可指定測試包的manifest文件的<instrumentation>標籤內容爲Instrumentation 或其子類。InstrumentationTestRunner的代碼在共享庫android.test.runner中,因此它一般沒有連接到你的代 碼,必須在<uses-library>標籤中指定。一般不須要手動去設定,Eclipse ADT以及android 命令行工具都會自動生成它們而且把它們加到測試包的manifest文件中。

注意:若是使用的是InstrumentationTestRunner以外的test runner,必須修改<instrumentation>標籤並指向你想使用的類。

要運行InstrumentationTestRunner類必須用android 工具調用內部系統類。Eclipse ADT中這些類都會被自動調用,命令行工具執行測試的時用Android Debug Bridge (adb)運行這些類。

系 統類加載和啓動測試包,殺掉被測試應用包正在運行的進程,而且從新加載被測試包的實體,而後它們把控制權交給 InstrumentationTestRunner,由它來執行測試包中的每一個test case。你也能夠經過Eclipse ADT中的setting或者命令行工具中的flag來控制哪些 test case或者方法的運行。

既不是系統類也不是 InstrumentationTestRunner運行被測試應用,而是test case。它要麼調用被測試包中的方法,要麼調用它本身的方法以改變被測試包生命週期。應用程序徹底由test case 控制,測試開始前由test case來初始化測試環境,

關於更多的運行測試,能夠參見 Testing from Eclipse with ADT和 Testing from Other IDEs

查看測試結果

Android 測試框架返回測試結果給啓動測試的工具。在eclipse ADT中結果會在新的JUnit視圖面板中顯示,命令行會在STDOUT中顯示。二者均可以看到顯示每一個test case 名字和你所運行的方法的小結,同時會看到全部失敗的斷言,其中包含指向產生失敗的測試代碼所在行的連接。失敗的斷言同時也會列出指望值和實際值。

測試結果根據IDE不一樣而有不一樣。


monkey 和 monkeyrunner


SDK提供了兩個應用測試工具:

  • UI/Application Exerciser Monkey,一般稱爲"monkey",向設備發送僞隨機事件流(如擊鍵、觸控、手勢)的命令行工具。你能夠經過Android Debug Bridge (adb)來運行它,對應用進行壓力測試而後報告遇到的錯誤。你能夠經過每次使用相同的隨機數種子來重複步驟。

  • monkeyrunner是一套API,基於Python。該API包含以下功能:鏈接到設備、安裝和卸載軟件包、截圖、比較圖片、運行應用對應的測試應用。經過該API,你能夠寫出功能強大複雜的測試,經過命令行工具monkeyrunner來運行。

 

處理包名

 

測試環境須要同時處理android應用包名和java包標識符。它們都使用一樣的命名格式,可是表明着徹底不一樣的實體。

android 包名是.apk文件對應的一個獨一無二的系統名字,由應用包的manifest文件中<manifest>標籤中 的"android:package"屬性來設定。測試包的名字必須和被測試包的名字不一樣,一般android工具會用被測試包的名字後加 上".test"來做爲測試包的名字。

測試包也會用包名來定位它所測試的應用,元由測試包的manifest文件中<instrumentation>素的"android:targetPackage"屬性設定。


java包標識符對應源文件,包名反映了源文件所在目錄,它同時會影響類與成員間彼此的可訪問性。

android 工具會幫助你設定測試包的名字。根據你的輸入,工具會設定測試包的名字以及測試的目標包的名字。只有在被測試應用工程已經存在的狀況下這些工具纔會起做用。

默 認狀況下,這些工具會將測試類的包標識符設定爲與被測試應用的包標示符一致。若是你想暴露被測試包中的一些成員你可能須要作一些修改。若是要修改,只修改 java 包標示符,不要修改android 包名,只修改test case 的源文件而不要修改測試包中R.java文件的包名,由於修改它會形成與被測試包中的R.java類衝突。不要將測試包的android包名修改爲和它所 測試的應用的包名同樣,由於這樣它們的名字在系統中再也不是獨一無二的。

測試內容

What To Test詳細地描述了android應用中應該被測試的關鍵功能以及可能會影響該功能的情況。

大部分的單元測試是專門針對你正在測試的andorid組件。Activity Testing、 Content Provider Testing和 Service Testing中都有一章節列出「須要測試什麼」。

儘可能在真實的設備上運行這些測試。其次用Android Emulator來加載已經配置好你所但願測試的硬件、屏幕、版本的android vitual device。

 

參考資料

http://developer.android.com/intl/zh-cn/tools/testing/testing_android.html

http://www.uml.org.cn/mobiledev/201306074.asp


微博 http://weibo.com/cizhenshi 做者博客:http://my.oschina.net/u/1433482/ python測試開發精華羣 291184506 PythonJava單元白盒測試 144081101



谷歌安卓測試實例介紹

AndroidJunitRunnerSample

實例下載地址:https://github.com/googlesamples/android-testing/tree/master/runner/AndroidJunitRunnerSample

記錄時間:2016-02-19

使用方法:在Android Studio中導入根目錄,android-testing/runner/AndroidJunitRunnerSample

app/build.gradle內容以下:

apply plugin: 'com.android.application'
android {
    compileSdkVersion 23
    buildToolsVersion '23.0.2'
    defaultConfig {
        applicationId "com.example.android.testing.androidjunitrunnersample"
        minSdkVersion 8
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    productFlavors {
    }
}
dependencies {
    // App's dependencies, including test
    compile 'com.android.support:support-annotations:23.0.1'
    compile 'com.google.guava:guava:18.0'
    // Testing-only dependencies
    // Force usage of support annotations in the test app, since it is internally used by the runner module.
    androidTestCompile 'com.android.support:support-annotations:23.0.1'
    androidTestCompile 'com.android.support.test:runner:0.4.1'
    androidTestCompile 'com.android.support.test:rules:0.4.1'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
}


類Calculator的內容以下:

package com.example.android.testing.androidjunitrunnersample;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * A simple calculator with a basic set of operations.
 */
public class Calculator {

    public enum Operator {ADD, SUB, DIV, MUL}
    
    /**
     * Addition operation
     */
    public double add(double firstOperand, double secondOperand) {
        return firstOperand + secondOperand;
    }
    /**
     * Substract operation
     */
    public double sub(double firstOperand, double secondOperand) {
        return firstOperand - secondOperand;
    }
    
    /**
     * Divide operation
     */
    public double div(double firstOperand, double secondOperand) {
        checkArgument(secondOperand != 0, "secondOperand must be != 0, you cannot divide by zero");
        return firstOperand / secondOperand;
    }
    
    /**
     * Multiply operation
     */
    public double mul(double firstOperand, double secondOperand) {
        return firstOperand * secondOperand;
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<!--
  Copyright 2015 The Android Open Source Project
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
      http://www.apache.org/licenses/LICENSE-2.0
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
  -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.android.testing.androidjunitrunnersample" >
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".CalculatorActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

類CalculatorActivity

package com.example.android.testing.androidjunitrunnersample;

import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

/**
 * {@link android.app.Activity} which contains a simple calculator. Numbers can be entered in the
 * two {@link EditText} fields and result can be obtained by pressing one of the
 * operation {@link Button}s at the bottom.
 */
public class CalculatorActivity extends Activity {

    private static final String TAG = "CalculatorActivity";
    
    private Calculator mCalculator;
    
    private EditText mOperandOneEditText;
    private EditText mOperandTwoEditText;
    
    private TextView mResultTextView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_calculator);
        mCalculator = new Calculator();
        mResultTextView = (TextView) findViewById(R.id.operation_result_text_view);
        mOperandOneEditText = (EditText) findViewById(R.id.operand_one_edit_text);
        mOperandTwoEditText = (EditText) findViewById(R.id.operand_two_edit_text);
    }
    
    /**
     * OnClick method that is called when the add {@link Button} is pressed.
     */
    public void onAdd(View view) {
        compute(Calculator.Operator.ADD);
    }
    
    /**
     * OnClick method that is called when the substract {@link Button} is pressed.
     */
    public void onSub(View view) {
        compute(Calculator.Operator.SUB);
    }
    
    /**
     * OnClick method that is called when the divide {@link Button} is pressed.
     */
    public void onDiv(View view) {
        try {
            compute(Calculator.Operator.DIV);
        } catch (IllegalArgumentException iae) {
            Log.e(TAG, "IllegalArgumentException", iae);
            mResultTextView.setText(getString(R.string.computationError));
        }
    }
    
    /**
     * OnClick method that is called when the multiply {@link Button} is pressed.
     */
    public void onMul(View view) {
        compute(Calculator.Operator.MUL);
    }
    private void compute(Calculator.Operator operator) {
        double operandOne;
        double operandTwo;
        try {
            operandOne = getOperand(mOperandOneEditText);
            operandTwo = getOperand(mOperandTwoEditText);
        } catch (NumberFormatException nfe) {
            Log.e(TAG, "NumberFormatException", nfe);
            mResultTextView.setText(getString(R.string.computationError));
            return;
        }
        String result;
        switch (operator) {
            case ADD:
                result = String.valueOf(mCalculator.add(operandOne, operandTwo));
                break;
            case SUB:
                result = String.valueOf(mCalculator.sub(operandOne, operandTwo));
                break;
            case DIV:
                result = String.valueOf(mCalculator.div(operandOne, operandTwo));
                break;
            case MUL:
                result = String.valueOf(mCalculator.mul(operandOne, operandTwo));
                break;
            default:
                result = getString(R.string.computationError);
                break;
        }
        mResultTextView.setText(result);
    }
    
    /**
     * @return the operand value which was entered in an {@link EditText} as a double
     */
    private static Double getOperand(EditText operandEditText) {
        String operandText = getOperandText(operandEditText);
        return Double.valueOf(operandText);
    }
    
    /**
     * @return the operand text which was entered in an {@link EditText}.
     */
    private static String getOperandText(EditText operandEditText) {
        String operandText = operandEditText.getText().toString();
        if (TextUtils.isEmpty(operandText)) {
            throw new NumberFormatException("operand cannot be empty!");
        }
        return operandText;
    }
}


佈局文件:activity_calculator.xml

<?xml version="1.0" encoding="utf-8"?>
<!--
  Copyright 2015 The Android Open Source Project
  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at
      http://www.apache.org/licenses/LICENSE-2.0
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
  -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context=".CalculatorActivity">
    <EditText android:id="@+id/operand_one_edit_text"
        android:hint="@string/type_operand_one_hint"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="numberDecimal"/>
    <EditText android:id="@+id/operand_two_edit_text"
        android:hint="@string/type_operant_two_hint"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="numberDecimal"/>
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button android:id="@+id/operation_add_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/add_operation_text"
            android:onClick="onAdd"/>
        <Button android:id="@+id/operation_sub_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/sub_operation_text"
            android:layout_toRightOf="@id/operation_add_btn"
            android:layout_toEndOf="@id/operation_add_btn"
            android:onClick="onSub"/>
        <Button android:id="@+id/operation_div_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/div_operation_text"
            android:layout_below="@id/operation_add_btn"
            android:onClick="onDiv"/>
        <Button android:id="@+id/operation_mul_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/mul_operation_text"
            android:layout_below="@id/operation_add_btn"
            android:layout_toRightOf="@id/operation_add_btn"
            android:layout_toEndOf="@id/operation_add_btn"
            android:onClick="onMul"/>
    </RelativeLayout>
    <TextView android:id="@+id/operation_result_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="@dimen/activity_vertical_margin"/>
</LinearLayout>

總共有6個測試文件,其中HintMatcher.java、OperationHintInstrumentationTest.java、OperationHintLegacyInstrumentationTest.java基於junit3,不建議涉及。


CalculatorAddParameterizedTest包含了參數化(數據驅動), 只測試相加,實際和傳統的Junit測試相似,這種測試不建議放入Android test。

package com.example.android.testing.androidjunitrunnersample;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import android.test.suitebuilder.annotation.SmallTest;

import java.lang.Iterable;
import java.util.Arrays;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.runners.Parameterized.Parameters;


/**
 * JUnit4 tests for the calculator's add logic.
 *
 * <p> This test uses a Junit4s Parameterized tests features which uses annotations to pass
 * parameters into a unit test. The way this works is that you have to use the {@link Parameterized}
 * runner to run your tests.
 * </p>
 */
@RunWith(Parameterized.class)
@SmallTest
public class CalculatorAddParameterizedTest {
    /**
     * @return {@link Iterable} that contains the values that should be passed to the constructor.
     * In this example we are going to use three parameters: operand one, operand two and the
     * expected result.
     */
    @Parameters
    public static Iterable<Object[]> data() {
        return Arrays.asList(new Object[][]{
                {0, 0, 0},
                {0, -1, -1},
                {2, 2, 4},
                {8, 8, 16},
                {16, 16, 32},
                {32, 0, 32},
                {64, 64, 128}});
    }
    
    private final double mOperandOne;
    private final double mOperandTwo;
    private final double mExpectedResult;
    
    private Calculator mCalculator;
    
    /**
     * Constructor that takes in the values specified in
     * {@link CalculatorAddParameterizedTest#data()}. The values need to be saved to fields in order
     * to reuse them in your tests.
     */
    public CalculatorAddParameterizedTest(double operandOne, double operandTwo,
            double expectedResult) {
        mOperandOne = operandOne;
        mOperandTwo = operandTwo;
        mExpectedResult = expectedResult;
    }
    
    @Before
    public void setUp() {
        mCalculator = new Calculator();
    }
    
    @Test
    public void testAdd_TwoNumbers() {
        double resultAdd = mCalculator.add(mOperandOne, mOperandTwo);
        assertThat(resultAdd, is(equalTo(mExpectedResult)));
    }
}

CalculatorInstrumentationTest.java,該例子演示了Espresso的使用,同時對通用的測試操做作了精巧的封裝,是個比較好的學習對象:

/*
 * Copyright 2015, The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
package com.example.android.testing.androidjunitrunnersample;

import junit.framework.TestSuite;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
import org.junit.runner.RunWith;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.runner.AndroidJUnitRunner;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.LargeTest;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.closeSoftKeyboard;
import static android.support.test.espresso.action.ViewActions.typeText;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;

/**
 * JUnit4 Ui Tests for {@link CalculatorActivity} using the {@link AndroidJUnitRunner}.
 * This class uses the JUnit4 syntax for tests.
 * <p>
 * With the new AndroidJUnit runner you can run both JUnit3 and JUnit4 tests in a single test
 * suite. The {@link AndroidRunnerBuilder} which extends JUnit's
 * {@link AllDefaultPossibilitiesBuilder} will create a single {@link
 * TestSuite} from all tests and run them.
 */
@RunWith(AndroidJUnit4.class)
@LargeTest
public class CalculatorInstrumentationTest {

    /**
     * A JUnit {@link Rule @Rule} to launch your activity under test. This is a replacement
     * for {@link ActivityInstrumentationTestCase2}.
     * <p>
     * Rules are interceptors which are executed for each test method and will run before
     * any of your setup code in the {@link Before @Before} method.
     * <p>
     * {@link ActivityTestRule} will create and launch of the activity for you and also expose
     * the activity under test. To get a reference to the activity you can use
     * the {@link ActivityTestRule#getActivity()} method.
     */
    @Rule
    public ActivityTestRule<CalculatorActivity> mActivityRule = new ActivityTestRule<>(
            CalculatorActivity.class);
            
    @Test
    public void noOperandShowsComputationError() {
        final String expectedResult = mActivityRule.getActivity().getString(R.string.computationError);
        onView(withId(R.id.operation_add_btn)).perform(click());
        onView(withId(R.id.operation_result_text_view)).check(matches(withText(expectedResult)));
    }
    @Test
    public void typeOperandsAndPerformAddOperation() {
        performOperation(R.id.operation_add_btn, "16.0", "16.0", "32.0");
    }
    
    @Test
    public void typeOperandsAndPerformSubOperation() {
        performOperation(R.id.operation_sub_btn, "32.0", "16.0", "16.0");
    }
    
    @Test
    public void typeOperandsAndPerformDivOperation() {
        performOperation(R.id.operation_div_btn, "128.0", "16.0", "8.0");
    }
    
    @Test
    public void divZeroForOperandTwoShowsError() {
        final String expectedResult = mActivityRule.getActivity().getString(
                R.string.computationError);
        performOperation(R.id.operation_div_btn, "128.0", "0.0", expectedResult);
    }
    
    @Test
    public void typeOperandsAndPerformMulOperation() {
        performOperation(R.id.operation_mul_btn, "16.0", "16.0", "256.0");
    }
    
    private void performOperation(int btnOperationResId, String operandOne,
            String operandTwo, String expectedResult) {
        // Type the two operands in the EditText fields
        onView(withId(R.id.operand_one_edit_text)).perform(typeText(operandOne),
                closeSoftKeyboard());
        onView(withId(R.id.operand_two_edit_text)).perform(typeText(operandTwo),
                closeSoftKeyboard());
                
        // Click on a given operation button
        onView(withId(btnOperationResId)).perform(click());
        
        // Check the expected test is displayed in the Ui
        onView(withId(R.id.operation_result_text_view)).check(matches(withText(expectedResult)));
    }
}


類CalculatorTest也是傳統Junit測試的展現,比第一個類CalculatorAddParameterizedTest多了些測試而已:

package com.example.android.testing.androidjunitrunnersample;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import android.support.test.runner.AndroidJUnit4;
import android.test.suitebuilder.annotation.SmallTest;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

/**
 * JUnit4 unit tests for the calculator logic.
 */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class CalculatorTest {

    private Calculator mCalculator;
    
    @Before
    public void setUp() {
        mCalculator = new Calculator();
    }
    
    @Test
    public void addTwoNumbers() {
        double resultAdd = mCalculator.add(1d, 1d);
        assertThat(resultAdd, is(equalTo(2d)));
    }
    
    @Test
    public void subTwoNumbers() {
        double resultSub = mCalculator.sub(1d, 1d);
        assertThat(resultSub, is(equalTo(0d)));
    }
    
    @Test
    public void subWorksWithNegativeResult() {
        double resultSub = mCalculator.sub(1d, 17d);
        assertThat(resultSub, is(equalTo(-16d)));
    }
    
    @Test
    public void divTwoNumbers() {
        double resultDiv = mCalculator.div(32d,2d);
        assertThat(resultDiv, is(equalTo(16d)));
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void divDivideByZeroThrows() {
        mCalculator.div(32d,0d);
    }
    
    @Test
    public void mulTwoNumbers() {
        double resultMul = mCalculator.mul(32d, 2d);
        assertThat(resultMul, is(equalTo(64d)));
    }
}

相關參考資料:https://io2015codelabs.appspot.com/codelabs/android-studio-testing#1

中文參考:http://www.jianshu.com/p/03118c11c199

http://tech.meituan.com/Android_unit_test.html

http://www.infoq.com/cn/articles/android-unit-test

相關文章
相關標籤/搜索