分類
Android

Android Monkey 介紹與使用

描述

Monkey主要是用來做穩定性或是壓力測試,它可以執行在實體裝置或是模擬器上,藉由產生一系列的隨機事件(e.g., touch, click, gestures)來測試應用程序。


基本語法

adb shell monkey [options] <event-count>

[options] 為參數,<event-count>為事件數。


常用範例介紹

1.啟動monkey
直接執行 monkey, 發送event count 數量的事件:

adb shell monkey <event count>

 
2.停止monkey
2-1.搜尋 monkey pid

adb shell ps | grep 'monkey'

2-2.kill monkey by pid

adb shell kill <Step 1.找到的 pid>

Notectrl + c無法停止 monkey
 
3.執行在 com.package.name 上, 發送 event count 數量的事件

adb shell monkey -p com.package.name <event count>

 
4.執行在 com.package.name , 發送 event count 數量的事件,顯示最詳細的訊息

adb shell monkey -v-v-v -p com.package.name <event count>

 
5.執行在 com.package.name , 發送 event count 數量的事件,每個事件等待1秒, 顯示最詳細的訊息,事件隨機分配。

adb shell monkey -v-v-v -p com.package.name --throttle 1000 <event count>

 
6.執行在 com.package.name , 發送 event count 數量的事件,每個事件等待1秒,調整 touch 事件為50%,剩餘百分比隨機分配,顯示最詳細的訊息

adb shell monkey -v-v-v -p com.package.name --pct-touch 50 --throttle 1000 <event count>

 
7.執行在 com.package.name 上, 發送 event count 數量的事件,每個事件等待1秒,調整 touch 事件 50%,motion事件 25%,剩餘百分比隨機分配,顯示最詳細的訊息

adb shell monkey -v-v-v -p com.package.name --pct-touch 50 --pct-motion 25 --throttle 1000 <event count>

 



monkey
參數[options]介紹:

參數總共分4大類,為一般(General)類,事件(Events)類,限制(Constraints)類,除錯(Debugging)類。

一般類

-h : 幫助訊息

adb shell monkey -h
usage: monkey [-p ALLOWED_PACKAGE [-p ALLOWED_PACKAGE] ...]
              [-c MAIN_CATEGORY [-c MAIN_CATEGORY] ...]
              [--ignore-crashes] [--ignore-timeouts]
              [--ignore-security-exceptions]
              [--monitor-native-crashes] [--ignore-native-crashes]
              [--kill-process-after-error] [--hprof]
              [--pct-touch PERCENT] [--pct-motion PERCENT]
              [--pct-trackball PERCENT] [--pct-syskeys PERCENT]
              [--pct-nav PERCENT] [--pct-majornav PERCENT]
              [--pct-appswitch PERCENT] [--pct-flip PERCENT]
              [--pct-anyevent PERCENT] [--pct-pinchzoom PERCENT]
              [--pkg-blacklist-file PACKAGE_BLACKLIST_FILE]
              [--pkg-whitelist-file PACKAGE_WHITELIST_FILE]
              [--wait-dbg] [--dbg-no-events]
              [--setup scriptfile] [-f scriptfile [-f scriptfile] ...]
              [--port port]
              [-s SEED] [-v [-v] ...]
              [--throttle MILLISEC] [--randomize-throttle]
              [--profile-wait MILLISEC]
              [--device-sleep-time MILLISEC]
              [--randomize-script]
              [--script-log]
              [--bugreport]
              [--periodic-bugreport]
              COUNT

 
-v:顯示log訊息
越多 -v,顯示的訊息越多,最多可以有3-v 

adb shell monkey -v-v-v 10
:Monkey: seed=0 count=10
:IncludeCategory: android.intent.category.LAUNCHER
:IncludeCategory: android.intent.category.MONKEY
// Event percentages:
//   0: 15.0%
//   1: 10.0%
//   2: 2.0%
//   3: 15.0%
//   4: -0.0%
//   5: 25.0%
//   6: 15.0%
//   7: 2.0%
//   8: 2.0%
//   9: 1.0%
//   10: 13.0%
:Switch: #Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10200000;component=com.google.android.street/.Street;end
    // Allowing start of Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.google.android.street/.Street } in package com.google.android.street
:Sending Flip keyboardOpen=false
:Sending Touch (ACTION_DOWN): 0:(313.0,367.0)
:Sending Touch (ACTION_UP): 0:(313.19876,367.49512)
Events injected: 10
:Sending rotation degree=0, persist=false
:Dropped: keys=0 pointers=0 trackballs=0 flips=0 rotations=0
## Network stats: elapsed time=247ms (0ms mobile, 247ms wifi, 0ms not connected)
// Monkey finished

2seed=0代表事件使用的隨機數,如果指定相同的隨機數,將會產生相同的事件。
count=10代表事件數量
第3~4行 :IncludeCategory: android.intent.category.LAUNCHER
:IncludeCategory: android.intent.category.MONKEY
代表 monkey 會去執行指定category的類別,預設是LAUNCHER or MONKEY
當在指令中使用 -c 參數時會顯示在這裡,若找不到指定的category會顯示以下訊息。

// Warning: no activities found for category android.intent.category.test
** No activities found to run, monkey aborted.

516行:事件比例,對照如下

// Event percentages:
// 0: 15.0% (--pct-touch )
// 1: 10.0% (--pct-motion)
// 2: 2.0% (--pct-pinchzoom)
// 3: 15.0% (--pct-trackball)
// 4: -0.0% (--pct-rotation)
// 5: 25.0% (--pct-nav)
// 6: 15.0% (--pct-majornav)
// 7: 2.0% (--pct-syskeys)
// 8: 2.0% (--pct-appswitch)
// 9: 1.0% (--pct-flip)
// 10: 13.0%(--pct-anyevent)

1722行:monkey 切換訊息。其中包括 actioncategorycomponent的訊息。action category 請參考 android developer 參考。而 component 即為啟動的 package全名和 activity名稱。
23行之後包含相關的測試訊息觸碰座標,旋轉螢幕等等。


事件類

-s:隨機數,指定相同的隨機數會有相同的事件產生。
–throttle:每個事件之間延遲的時間,以mill second 來計算,預設沒有延遲。
–pct-touch:觸碰事件,觸碰事件為按下放開事件。
–pct-motion:運動事件,運動事件為按下事件加上一連串的隨機事件最後加上放開事件。
–pct-trackball:軌跡球事件,軌跡球事件為一個或多個亂數動作。
–pct-nav:基本導航事件,基本導航事件為裝置的上,下,左,右輸入。
–pct-majornav:主要導航事件,主要導航事件為引發圖形介面的動作(e.g.,返回鍵,或選單鍵)
–pct-syskeys:系統事件,系統按鍵事件為home鍵,返回鍵,撥號鍵,音量鍵等等。
–pct-appswitchactivity啟動事件, activity啟動事件為影響monkey 最大化的平均切換同一個package內的所有activity 的機率。
–pct-anyevent:其他事件,包含以上沒有提到事件。

//執行在 com.example.mypackage 上, 發送 100 數量的事件,調整 touch 事件為10%,motion事件20%,軌跡球事件為 15%,剩餘百分比隨機分配,顯示最詳細的訊息
adb shell monkey -v-v-v -p com.example.mypackage -s 999 --pct-touch 10 --pct-motion 20 --pct-trackball 15 100
限制類

-p:指定 monkey 運作的 package,可以有一個到多個 packagemonkey會啟動在該package內的activity,如果 app 需要存取其他的 packagee.g., 聯絡人)。也必須在此指定。預設是隨機啟動該裝置中任何一個package

//執行在 com.example.mypackage 上, 發送 100 數量的事件,事件隨機分配,顯示最詳細的訊息
adb shell monkey -v-v-v -p com.example.mypackage 100

 
-c: 指定 monkey 運作的 category,通常可用來指定 monkey 啟動的 activitycategory AndroidManifest.xml 中設定(e.g.,預設為啟動Intent.CATEGORY_LAUNCHER or Intent.CATEGORY_MONKEY

//執行在 com.example.mypackage 上, 限制 category 為 LAUNCHER 以及 MONKEY,發送 100 數量的事件,事件隨機分配,顯示最詳細的訊息
adb shell monkey -v-v-v -p com.example.mypackage -c android.intent.category.LAUNCHER -c android.intent.category.MONKEY 100

AndroidManifest.xml example

...
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.mypackage.MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
...

第14行即為 category 指定的方式,因此 monkey 將會執行在com.example.mypakcage.MainActivity 。

除錯類

-hprof:在 monkey 執行事件的前後產生報告,報告大約有 5mb,路徑為data/misc
–ignore-crashesmonkey 預設當測試發生 exception crash 會立即停止,當設定該參數 monkey將不會停止。
–ignore-timeoutsmonkey 預設當測試發生 ANR會立即停止,當設定該參數 monkey將不會停止。
–ignore-security-exceptionsmonkey 預設當測試發生 permission error會立即停止,當設定該參數 monkey將不會停止。
–kill-process-after-errormonkey 預設當測試發生錯誤時不會停止啟動的程序,當設定該參數 monkey將會停止該程序。Note: monkey 成功完成測試後,也不會停止該程序,只會保留在測試結束的最後一個階段。
–montion-native-crashes:監看並報告當測試發生native code 的錯誤。
–wait-dbg:停止執行中的 monkey直到有除錯器連接為止。
–dbg-no-events:設定後,monkey 進入activity 前不會產生事件,一般會和 -v , -p-throttle 一起使用來監視 packages 之間的切換。

分類
Android

Robotium 使用介紹

Robotium 為專門測試 android ui 的 framework,詳細介紹參考官網
本篇介紹如何導入 Robotium,以及建立測試 Activity 的 Robotium 模板,最後是 Robotium 常用函式介紹。


如何導入 Robotium

  1. Robotium 的使用環境和 android test project 非常類似,因此先建立 android test project。
    建立 android test project 詳細步驟請參考這篇
  2. 在第1步建立的 android test project 根目錄中建立 libs 資料夾,並將 Robotium 提供的 jar 檔放入 libs ,android 會自動幫你做好其他事。

建立測試 Activity 的 Robotium 模板

完成導入 Robotium 之後,你應該有個 test case,再將以下的 mSolo 加入。
Solo 為 Robotium 提供的類別,大部分測試功能都由它開始。

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity>
{
    private Solo mSolo;
    public MainActivityTest(String name) {
        super(MainActivity.class);
        setName(name);
    }
    public MainActivityTest()
    {
        this(MainActivityTest.class.getSimpleName());
    }
    protected void tearDown() throws Exception
    {
        mSolo.finishOpenedActivities();
        super.tearDown();
    }
    protected void setUp() throws Exception
    {
        super.setUp();
        mSolo = new Solo(getInstrumentation(), getActivity());
    }
    public void testPreconditions()
    {
       assertNotNull(mSolo);
    }
}

第24行初始化 mSolo
第17行銷毀 mSolo
以上就是測試 activity 的 Robotium 模板


Robotium 常用函式介紹

使用 Robotium 來寫測試主要分為3個步驟:

  1. 取得 UI Component
  2. 操作 UI Component
  3. 使用斷言(Assert)判斷操作結果是否符合預期

接下來依序介紹在 Robotium 中各個步驟如何進行

取得 UI Component

在 Robotium 主要有2種取得 UI Component 的方式,第1種為根據 UI Component 的 ID 來取得,適合用於 UI Component 具有唯一的 ID 情況。

android.view.View getView(int id)

Returns a View matching the specified resource id.
android.view.View getView(String id)

Returns a View matching the specified resource id.

Example

...
LinearLayout mainView = (LinearLayout) mSolo.getView(R.id.main_view);//R.id.main_view 為 resource id

UI Component 的內容值可以當作識別的方式,但這種方式只支援 Button, EditText, TextView, 適合用於 UI Component 具有內容值的情況。

android.widget.Button getButton(String text)

Returns a Button displaying the specified text.
android.widget.EditText getEditText(String text)

Returns an EditText displaying the specified text.
android.widget.TextView getText(String text)

Returns a TextView displaying the specified text.

Example

...
Button exitButton = mSolo.getButton("Exit");//Exit 為顯示在 button 上的內容

UI Component 的 Tag 可以當作識別的方式,適合用於 UI Component 具有 Tag 的情況下。

android.view.View getView(Object tag)

Returns a View matching the specified tag.

第2種為根據 UI Component 的類型進行過濾再根據索引來取得,這種情況適用於 UI Component 沒有可以識別的 ID 情況。以下第1個方法為回傳所有類型的 views, 第2個回傳指定類型的 views, 第3個回傳指定類型並指定 parent 的 views

ArrayList<android.view.View> getCurrentViews()

Returns an ArrayList of the Views currently displayed in the focused Activity or Dialog.
<T extends android.view.View>
ArrayList<T>
getCurrentViews(Class<T> classToFilterBy)

Returns an ArrayList of Views matching the specified class located in the focused Activity or Dialog.
<T extends android.view.View>
ArrayList<T>
getCurrentViews(Class<T> classToFilterBy, android.view.View parent)

Returns an ArrayList of Views matching the specified class located under the specified parent.

使用上述的方法取得 view 的群集之後,再使用以下方法取得個別的 view。
需要注意的是 index 該 index 為子視圖的順序(index 從零開始由左到右或上到下遞增)

android.widget.Button getButton(int index)

Returns a Button matching the specified index.
android.widget.EditText getEditText(int index)

Returns an EditText matching the specified index.
android.widget.ImageView getImage(int index)

Returns an ImageView matching the specified index.
android.widget.ImageButton getImageButton(int index)

Returns an ImageButton matching the specified index.
android.widget.TextView getText(int index)

Returns a TextView matching the specified index.
<T extends android.view.View>
T
getView(Class<T> viewClass, int index)

Returns a View matching the specified class and index.

Example

...
LinearLayout mainView = (LinearLayout) mSolo.getView(R.id.main_view);
List<Button> buttons = mSolo.getCurrentViews(Button.class, mainView);
for (int i=0; i<buttons.size(); ++i) {
    Button button = mSolo.getButton(i);
}
//以上的 source code 即是取得 mainView 中的所有 button

操作 UI Component

Robotium 提供的操作共有點擊,長按,輸入文字,拖曳,滾動,搜尋,等待。
點擊&長按
點擊和長按的方法大致上各為clickOnXXX, clickLongOnXXX,支援的種類有 View, Button, EditText, ImageView, ImageButton, CheckBox, MenuItem, RadioButton, ToggleButton, Screen座標等等 。
詳細參考官網
Example :

...
mSolo.clickOnButton("Exit");//點擊 Exit button

輸入和清除文字
各為 typeText 以及 enterText,都是對 EditText 輸入文字,2者不同為 type 會一個一個的輸入,而 enter 為直接全部輸入 ,清除則是只對 EditText 清除 (cleanEditText)。
詳細參考官網
Example :

...
EditText numberEditText = mSolo.getEditText("number");//取得內容為 number 的 EditText
mSolo.typeText(numberEditText, "123");//在 EditText 填入 123

拖曳

void drag(float fromX, float toX, float fromY, float toY, int stepCount)

Simulate touching the specified location and dragging it to a new location.
...
mSolo.drag(0.0f, 50.0f, 0.0f, 50.0f, 100);

需要注意的是原點從左上角開始算起。
滾動
滾動為 ScrollXXX,基本的有 ScrollDown, ScrollUp 往上下滾動一段。
ScrollToBottom, ScrollToTop 往頂端底端滾動。
ScrollToSide 滾動至左或右。進階可以指定滾動的距離。
支援的種類有 view, ListView, RecyclerView,
詳細參考官網

mSolo.scrollDown();

搜尋
searchXXX,搜尋的目的為確認 UI Compenent 是否存在於 Screen 上,有的話才進行下一步操作。支援 Button, EditText, TextView, ToggleButton, 詳細參考官網
Example :

...
if(!mSolo.searchButton("Exit")){//尋找 Exit button 是否在螢幕上
    fail("Exit button should on screen");
}

等待
waitForXXX為等待系列,支援 activity, dialog open dialog close, text, view。
需要注意的是 waitFor 函式會在其內預設等待20秒,在20秒之內若等待條件成立的話回傳 true,反之回傳 false,也有提供自定義時間 e.g. mSolo.waitForLogMessage(“”,5000)。
比較特殊等待條件有 waitForLogMessage,這裡的 LogMessage 即為使用 Log 列印出來的內容
e.g. Log.d(TAG,”log message should print”);
就可使用 mSolo.waitForLogMessage(“log message should print”);
另一個是 waitForCondition , 可以自定義複雜條件以滿足特別的需求。
詳細參考官網
另外還有其他操作 e.g. 休息(sleep),擷圖(takeScreenShot),模擬按下實體按鍵, 檢查 UI 狀態(isXXX)。


使用斷言(Assert)判斷操作結果是否符合預期

因為 android test project 預設為使用 junit 來斷言,對於絕大部分的斷言情況來說已經足夠。Robotium 提供的斷言不多,只有4種(assertCurrentAcitvity, assertMemoryNotLow)。
Example :

...
assertTrue(mSolo.searchButton("Exit"));//斷言 Exit button 需要在螢幕上

 
 
 
 

分類
Android

使用 TDD 加入 android 測試案例

上一篇 建立測試案例以及測試 android activity 的模板 建立基本模板以及測試案例之後。本篇開始介紹如何使用 TDD 的方式加入測試。
TDD(測試驅動開發)起源於 XP(極限編程)主張先寫測試再寫代碼的循環,循環的步驟如下:

  1. 用代碼寫需求,必須符合單元測試。(寫測試代碼)
  2. 測試失敗。(因為還未產生產品代碼)。
  3. 編寫代碼,實現需求。(實現產品代碼)
  4. 測試通過。
  5. 重構。

TDD需要為每段產品代碼編寫測試案例,並讓測試案例優先,測試案例定義產品代碼需要做什麼,並保證產品代碼確實符合規定。
首先來建立需求清單。

  1. 被測程式可以取得時間並以數位方式顯示在螢幕上。
  2. 使用者藉由點擊按鈕來顯示時間。
  3. 使用者藉由點擊按鈕來離開程式,離開前必須出現對話框以確定是否真正離開。

被測程式以及測試程式已經在前幾篇建立完成,接下來開始 TDD。
藉由需求清單我們可以考慮螢幕需要兩個按鈕用來啟動顯示時間以及啟動對話框,另外還需要一個 TextView 用來顯示時間。
因此我們先寫測試來建立 UI component,在測試程式的 MainActivityTest 加入以下代碼

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity>
{
    private MainActivity mMainActivity;
    private Button mShowTimeButton;
    private Button mExitButton;
    private TextView mTimeView;
    public MainActivityTest() {
        super("com.example.targettestproject", MainActivity.class);
    }
    protected void setUp() throws Exception
    {
        super.setUp();
        mMainActivity = getActivity();
        initUIComponents();
    }
    private void initUIComponents()
    {
       mShowTimeButton = mMainActivity.findViewById(R.id.showtime_btn);
       mExitButton = mMainActivity.findViewById(R.id.exit_btn);
       mTimeView = mMainActivity.findViewById(R.id.time_view);
    }
    protected void tearDown() throws Exception
    {
        super.tearDown();
    }
    public void testPrecondition()
    {
        assertNotNull(mMainActivity);
        assertNotNull(mExitButton);
        assertNotNull(mShowTimeButton);
        assertNotNull(mTimeView);
    }
}

第5~7行即為被測程式中使用到的 UI component,保存在欄位中方便測試。
第17行在 setUp 呼叫初始化 UI component 的動作。
第35~37行斷言這些 UI component 不可為 null。
到目前為止完成 TDD 的第1個步驟寫測試,但因為被測程式中還不存在這些 UI component,因此現在測試程式還無法執行(測試失敗)。準備進行第3步(編寫代碼,實現需求)我們到測試程式中建立這些 UI component
在 TargetTestProject/src/com/example/targettestproject/MainActivity 加入以下 source code

public class MainActivity extends Activity
{
    private Button mShowTimeButton;
    private Button mExitButton;
    private TextView mTimeView;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        initUIComponents();
    }
    private void initUIComponents()
    {
       mShowTimeButton = (Button) findViewById(R.id.showtime_btn);
       mExitButton = (Button) findViewById(R.id.exit_btn);
       mTimeView = (TextView) findViewById(R.id.time_view);
    }
}

並開啟 TargetTestProject/res/layout/main.xml 加入以下 source code

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
    <TextView
        android:id="@+id/time_view"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_gravity="center"
        android:layout_weight="0.6"
        android:gravity="center"
        android:textSize="30sp" />
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="0.4"
        android:orientation="horizontal" >
        <Button
            android:id="@+id/showtime_btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            android:text="Show Time" />
        <Button
            android:id="@+id/exit_btn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="0.5"
            android:text="Exit" />
    </LinearLayout>
</LinearLayout>

完成 TDD 第3步編寫代碼實現需求。接著開始 TDD 第4步進行測試。輸入以下指令以執行測試

ant uninstall clean debug install test

輸出為

test:
     [echo] Running tests ...
     [exec]
     [exec] com.example.targettestproject.MainActivityTest:.
     [exec] Test results for InstrumentationTestRunner=.
     [exec] Time: 0.629
     [exec]
     [exec] OK (1 test)
     [exec]
     [exec]
BUILD SUCCESSFUL
Total time: 27 seconds

通過測試!!!
因為加入的 source code 相當簡單,並沒有發現其他程式碼怪味(Code Smell)。
第5步重構可以先忽略。上述過程為完成1次 TDD 循環的過程。
 
接著把剩下的需求轉換成測試案例,從第1項需求開始
“被測程式可以取得時間並以數位方式顯示在螢幕上。”
把 “取得時間” 轉換為 source code,在測試程式實作該測試案例。
MainActivityTest 加入以下 source code  (TDD 第1步和第2步)

    ...
    public void testGetTime()
    {
        String except = mMainActivity.getTime();
        assertNotNull(except);
    }

接著在 MainActivity 加入相對應的實作(TDD第3步)

    ...
    public String getTime()
    {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd kk:mm:ss", Locale.TAIWAN);
        return dateFormat.format(new Date());
    }

進行測試,輸入

ant uninstall clean debug install test

輸出如下

test:
[echo] Running tests ...
[exec]
[exec] com.example.targettestproject.MainActivityTest:..
[exec] Test results for InstrumentationTestRunner=..
[exec] Time: 1.257
[exec]
[exec] OK (2 tests)
[exec]
[exec]
BUILD SUCCESSFUL
Total time: 29 seconds

通過測試!!!(TDD第4步)
針對第1項需求的後半段 “以數位方式顯示在螢幕上” 代表 mTimeView 欄位必須能設定數值且數值必須正確才行。
TDD 第1步,加入測試案例。MainActivityTest 加入以下代碼

    ...
    public void testSetTimeToTimeView()
    {
        String time = mMainActivity.getTime();
        mTimeView.setText(time);
        assertEquals(time, mTimeView.getText());
    }

這一次不需要實作相對應的實作,因為 mTimeView.setText() 函式已經存在於android 本身。
執行測試,輸入

ant uninstall clean debug install test

測試失敗!!!輸出訊息如下

test:
     [echo] Running tests ...
     [exec]
     [exec] com.example.targettestproject.MainActivityTest:..
     [exec] Error in testSetTimeToTimeView:
     [exec] android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
     [exec]     at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:4925)
     [exec]     at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:950)
     [exec]     at android.view.View.requestLayout(View.java:15461)
     [exec]     at android.view.View.requestLayout(View.java:15461)
     [exec]     at android.view.View.requestLayout(View.java:15461)
     [exec]     at android.view.View.requestLayout(View.java:15461)
     [exec]     at android.view.View.requestLayout(View.java:15461)
     [exec]     at android.widget.TextView.checkForRelayout(TextView.java:6645)
     [exec]     at android.widget.TextView.setText(TextView.java:3733)
     [exec]     at android.widget.TextView.setText(TextView.java:3591)
     [exec]     at android.widget.TextView.setText(TextView.java:3566)
     [exec]     at com.example.targettestproject.MainActivityTest.testSetTimeToTimeView(MainActivityTest.java:67)
     [exec]     at java.lang.reflect.Method.invokeNative(Native Method)
     [exec]     at android.test.InstrumentationTestCase.runMethod(InstrumentationTestCase.java:214)
     [exec]     at android.test.InstrumentationTestCase.runTest(InstrumentationTestCase.java:199)
     [exec]     at android.test.ActivityInstrumentationTestCase2.runTest(ActivityInstrumentationTestCase2.java:192)
     [exec]     at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:192)
     [exec]     at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:177)
     [exec]     at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:555)
     [exec]     at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1619)
     [exec]
     [exec] Test results for InstrumentationTestRunner=...E
     [exec] Time: 1.909
     [exec]
     [exec] FAILURES!!!
     [exec] Tests run: 3,  Failures: 0,  Errors: 1
     [exec]
     [exec]
BUILD SUCCESSFUL
Total time: 31 seconds

第6行說明失敗原因,只能在 UI thread 改變 UI component 的值才行,在其他 thread 改變都會失敗。
有不少方法可以修正該錯誤,我們採取最間單的方式。在該測試案例上加上 @UiThreadTest,該註解說明其內的動作全都會執行在 UI thread 上。加上該註解

    @UiThreadTest
    public void testSetTimeToTimeView()
    {
        String time = mMainActivity.getTime();
        mTimeView.setText(time);
        assertEquals(time, mTimeView.getText());
    }

再次執行測試,輸入

ant uninstall clean debug install test

測試結果輸出

test:
[echo] Running tests ...
[exec]
[exec] com.example.targettestproject.MainActivityTest:...
[exec] Test results for InstrumentationTestRunner=...
[exec] Time: 2.156
[exec]
[exec] OK (3 tests)
[exec]
[exec]
BUILD SUCCESSFUL
Total time: 30 seconds

測試成功!!!
接著 TDD 第2項需求
“使用者藉由點擊按鈕來顯示時間”
在MainActivityTest加入以下測試案例

    ...
    public void testShowTimeByClickButton()
    {
        TouchUtils.clickView(this, mShowTimeButton);
        String exceptTime = mMainActivity.getTime();
        assertEquals(exceptTime, mTimeView.getText().toString());
    }

這次的測試案例不會出現失敗,因為需要的函式實作在之前的步驟已經完成。
進行測試。

ant uninstall clean debug install test

測試失敗!!!,輸出如下

test:
     [echo] Running tests ...
     [exec]
     [exec] com.example.targettestproject.MainActivityTest:...
     [exec] Failure in testShowTimeByClickButton:
     [exec] junit.framework.ComparisonFailure: expected:<[2017/02/20 10:06:30]> but was:<[]>
     [exec]     at com.example.targettestproject.MainActivityTest.testShowTimeByClickButton(MainActivityTest.java:80)
     [exec]     at java.lang.reflect.Method.invokeNative(Native Method)
     [exec]     at android.test.InstrumentationTestCase.runMethod(InstrumentationTestCase.java:214)
     [exec]     at android.test.InstrumentationTestCase.runTest(InstrumentationTestCase.java:199)
     [exec]     at android.test.ActivityInstrumentationTestCase2.runTest(ActivityInstrumentationTestCase2.java:192)
     [exec]     at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:192)
     [exec]     at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:177)
     [exec]     at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:555)
     [exec]     at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1619)
     [exec]
     [exec] Test results for InstrumentationTestRunner=....F
     [exec] Time: 4.306
     [exec]
     [exec] FAILURES!!!
     [exec] Tests run: 4,  Failures: 1,  Errors: 0
     [exec]
     [exec]
BUILD SUCCESSFUL
Total time: 44 seconds

從第6行可以看到預期的值為 2017/02/20 10:06:30 ,但是實際的值為空白。
雖然相關的函式實作已經完成,但將其連結的邏輯還未完成。
在目前的動作邏輯中,點擊按鈕並不會顯示時間。
因此我們回到被測專案(MainActivity)中加入以下代碼

public class MainActivity extends Activity implements OnClickListener
{
    private Button mShowTimeButton;
    private Button mExitButton;
    private TextView mTimeView;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        initUIComponents();
    }
    private void initUIComponents()
    {
       mShowTimeButton = (Button) findViewById(R.id.showtime_btn);
       mShowTimeButton.setOnClickListener(this);
       mExitButton = (Button) findViewById(R.id.exit_btn);
       mTimeView = (TextView) findViewById(R.id.time_view);
    }
    public String getTime()
    {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd kk:mm:ss", Locale.TAIWAN);
        return dateFormat.format(new Date());
    }
    @Override
    public void onClick(View view)
    {
       int uiId = view.getId();
       switch(uiId){
           case R.id.showtime_btn:
               mTimeView.setText(getTime());
           break;
       }
    }
}

第1行讓Activity 實作 OnClickListener 介面
第19行讓 mShowTimeButton 加入onClickListener 監聽器
第31~39行實作 onClick 函式內容即為將點擊按鈕連結到顯示時間
將動作的連結邏輯實作完成後,再來執行測試。

ant uninstall clean debug install test

測試通過!!!,輸出如下

test:
     [echo] Running tests ...
     [exec]
     [exec] com.example.targettestproject.MainActivityTest:....
     [exec] Test results for InstrumentationTestRunner=....
     [exec] Time: 1.931
     [exec]
     [exec] OK (4 tests)
     [exec]
     [exec]
BUILD SUCCESSFUL
Total time: 29 seconds

最後我們增加最後一項需求的測試案例。
“使用者藉由點擊按鈕出現對話框以確定是否真正離開程式。”
在這裡我們加入 Robotium 來更方便的寫測試,Robotium 是一個專門測試 android ui 的 framework,詳細介紹請到官網
導入的步驟其實就是將 Robotium 的 jar 檔加到測試專案。
首先在測試專案的根目錄建立 libs 資料夾,該資料夾中的 jar 檔都會被自動的連結到該專案中。到官網下載jar檔,目前(2017/02/20)最新的版本為 Robotium-5.6.3.jar
接著在 MainActivityTest 加入以下代碼

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity>
{
    private static final String DEBUG = MainActivityTest.class.getSimpleName();
    private Solo mSolo;
    protected void setUp() throws Exception
    {
        super.setUp();
        mSolo = new Solo(getInstrumentation(), getActivity());
        mMainActivity = getActivity();
        ...
    }
    protected void tearDown() throws Exception
    {
        mSolo.finishOpenedActivities();
        super.tearDown();
    }
    public void testShowExitDialogByClickExitButton()
    {
        mSolo.clickOnButton("Exit");
        assertTrue(mSolo.waitForDialogToOpen());
    }
    public void testNoButtonActionOnExitDialog()
    {
        mSolo.clickOnButton("Exit");
        boolean isExitDialogOpen = mSolo.waitForDialogToOpen();
        if (isExitDialogOpen) {
            mSolo.clickOnButton("no");
            assertTrue(mSolo.waitForDialogToClose());
        }
    }
    public void testYesButtonActionOnExitDialog()
    {
        mSolo.clickOnButton("Exit");
        boolean isExitDialogOpen = mSolo.waitForDialogToOpen();
        if (isExitDialogOpen) {
            mSolo.clickOnButton("yes");
            mSolo.sleep(5000);
            assertFalse(mSolo.searchButton("Show Time"));
        }
    }
}

第5行的 Solo 為 Robotium 最常使用的類別,我們需要一個欄位(mSolo)來保存它。
第10行測試前初始化 mSolo。
第17行測試後銷毀 mSolo。
第23行就是使用 Robotium 的方便之處,Robotium 會去目前頁面中搜尋符合 clickOnButton 的參數值(Exit),找到 button 的話就會去點擊它。
第24行 waitForDialogToOpen 代表會在預設的時間(20sec)內等待 dialog 顯示,如果20秒之內有 dialog 顯示就會回傳 true,反之則回傳 false。
第27行測試當點擊 exit 對話框的 no button 動作是否如預期。我們預期點擊 no button 之後,exit 對話框會關閉,回到主畫面。
第37行測試當點擊 exit 對話框上的 yes button 的動作是否如預期。我們預期點擊 yes button 之後,exit 對話框會關閉回到主畫面,MainActivity 會 finish,因此等待5秒再去尋找 Show Time 按鈕是不存在的。
寫完以上的測試代碼之後,開始寫產品代碼。
在 MainActivity 加入以下代碼

public class MainActivity extends Activity implements OnClickListener
{
    private Button mShowTimeButton;
    private Button mExitButton;
    private TextView mTimeView;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        initUIComponents();
    }
    private void initUIComponents()
    {
        mShowTimeButton = (Button) findViewById(R.id.showtime_btn);
        mShowTimeButton.setOnClickListener(this);
        mExitButton = (Button) findViewById(R.id.exit_btn);
        mExitButton.setOnClickListener(this);
        mTimeView = (TextView) findViewById(R.id.time_view);
    }
    public String getTime()
    {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd kk:mm:ss", Locale.TAIWAN);
        return dateFormat.format(new Date());
    }
    @Override
    public void onClick(View view)
    {
        int uiId = view.getId();
        switch (uiId) {
            case R.id.showtime_btn:
                mTimeView.setText(getTime());
                break;
            case R.id.exit_btn:
                showExitDialog();
                break;
        }
    }
    private void showExitDialog()
    {
        final AlertDialog exitDialog = new AlertDialog.Builder(this).create();
        exitDialog.setMessage("Exit app?");
        exitDialog.setButton(DialogInterface.BUTTON_POSITIVE, "yes", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which)
            {
               finish();
            }
        });
        exitDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "no", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which)
            {
              exitDialog.dismiss();
            }
        });
        exitDialog.show();
    }
}

第21行讓 mExitButton 設定 listener,對象為 MainActivity。
第39~41行建立點擊 mExitButton 動作,當點擊 mExitButton 之後會呼叫showExitDialog()
第45行即為 showExitDialog 函式本身,函式會建立含有2個 button (yes, no)的 dialog,並對這2個 button 建立相對應的動作。
完成產品代碼之後來執行測試,輸入

ant uninstall clean debug install test

測試通過!!!,輸出為

test:
     [echo] Running tests ...
     [exec]
     [exec] com.example.targettestproject.MainActivityTest:.......
     [exec] Test results for InstrumentationTestRunner=.......
     [exec] Time: 30.024
     [exec]
     [exec] OK (7 tests)
     [exec]
     [exec]
BUILD SUCCESSFUL
Total time: 1 minute 0 seconds

以上即為使用 TDD 來建立 android test 的過程,比較需要注意的是如果需求有對 UI Component 設定位置標準,如對齊,置中等等,就必須加入這些測試案例。
另一點為可以強化對斷言(assert)的條件,如以下 testGetTime 的測試案例

    public void testGetTime()
    {
        String except = mMainActivity.getTime();
        assertNotNull(except);
    }

我們僅對 getTime 測試不可為 null,事實上還可以測試它的格式是否符合標準(yyyy/MM/dd kk:mm:ss),測試是否位於合理範圍值等等。
 
 
 
 
 

分類
Android

建立測試案例以及測試 android activity 的模板

在上一篇 Create android test project by command line and eclipse 中我們建立被測專案(TargetTestProject)以及測試專案(TestProject),並在測試專案中加入一個測試Activity的 Test Case(MainActivityTest)。
但 MainActivityTest 不算是一個真正的 Test Case,因為裡面沒有任何測試案例。
因此本篇紀錄如何加入測試案例以及測試Activity的基本模板。
加入測試案例相當簡單,因為 android test framework 使用 Junit3,而 Junit3 會將所有公開,無回傳值並以 test 開頭的方法都認為是測試案例,e.g. public void testXXX()
因此在MainActivityTest 加入以下代碼,即為一個測試案例。

    public void testPrecondition()
    {
        fail("test Precondition");
    }

testPrecondition 會是基本模板中的一部分,裡面主要是測試所有前置條件是否通過。
而 fail(“test Precondition”),代表測試程式執行到這一定會失敗並印出提示內容。
接著執行測試專案,會看到以下輸出

test:
     [echo] Running tests ...
     [exec]
     [exec] com.example.targettestproject.MainActivityTest:
     [exec] Failure in testPrecondition:
     [exec] junit.framework.AssertionFailedError: test precondition
     [exec]     at com.example.targettestproject.MainActivityTest.testPrecondition(MainActivityTest.java:36)
     [exec]     at java.lang.reflect.Method.invokeNative(Native Method)
     [exec]     at android.test.InstrumentationTestCase.runMethod(InstrumentationTestCase.java:214)
     [exec]     at android.test.InstrumentationTestCase.runTest(InstrumentationTestCase.java:199)
     [exec]     at android.test.ActivityInstrumentationTestCase2.runTest(ActivityInstrumentationTestCase2.java:192)
     [exec]     at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:192)
     [exec]     at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:177)
     [exec]     at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:555)
     [exec]     at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1619)
     [exec]
     [exec] Test results for InstrumentationTestRunner=.F
     [exec] Time: 0.493
     [exec]
     [exec] FAILURES!!!
     [exec] Tests run: 1,  Failures: 1,  Errors: 0
     [exec]
     [exec]
BUILD SUCCESSFUL
Total time: 19 seconds

輸出的內容有點多,但重要的只有第6行 test precondition,代表測試程式目前如我們所預期的執行動作。
接著加入測試Activity的基本模板。

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
    private MainActivity mMainActivity;
    public MainActivityTest() {
        super("com.example.targettestproject", MainActivity.class);
    }
    protected void setUp() throws Exception
    {
        super.setUp();
        mMainActivity = getActivity();
    }
    protected void tearDown() throws Exception
    {
        super.tearDown();
    }
    public void testPrecondition()
    {
       assertNotNull(mMainActivity);
    }
}

第3行的 mMainActivity 即為被測專案的 MainActivity,我們需要一個欄位來保存它,因為之後需要藉由它取得其他元件。
第9行的 setUp 會在 ” 每個 ” 測試案例執行前呼叫,主要做初始化的動作,包含建立物件等等,因此可以看到mMainActivity 也在這裡初始化。
第15行的 tearDown 相對於 setUp,會在每個測試案例結束後執行,若測試案例有需要釋放資源,銷毀物件如 Service, ContentProvider 等等,請在這裡將他們指定為Null。
第20行測試前置條件 mMainActivity 不可為 Null。
以上即為測試Activity的基本模板。
若在各個函式之間加入log觀察執行順序,如下

    private static final String DEBUG_TAG = MainActivityTest.class.getSimpleName();
    protected void setUp() throws Exception
    {
        super.setUp();
        mMainActivity = getActivity();
        Log.d(DEBUG_TAG, "in setUp");
    }
    protected void tearDown() throws Exception
    {
        super.tearDown();
        Log.d(DEBUG_TAG, "in tearDown");
    }
    public void testPrecondition()
    {
        assertNotNull(mMainActivity);
        Log.d(DEBUG_TAG, "in testPrecondition");
    }

輸出如下

D/MainActivityTest( 9367): in setUp
D/MainActivityTest( 9367): in testPrecondition
D/MainActivityTest( 9367): in tearDown

可以看到 setUp 會在 testPrecondition 之前呼叫,而 tearDown 會在 testPrecondition 之後呼叫。
下一篇會在被測程式加入 UI 功能並在測試程式加入相對應的測試案例。
 
 
 

分類
Android

Create android test project by command line and eclipse

使用 Eclipse 建立 android test project

Step 1. 開啟 Eclipse -> File -> new -> other -> 輸入 android test project -> next
Step 2.輸入 測試專案名稱

Step 2.a 選擇 finish -> Step 5.

Step 2.b 選擇 next -> Step 3.

Step 3.選擇被測專案

Step 3.a 選擇 next -> Step 4.

Step 3.b 選擇 finish -> Step 5.

Step 4.選擇 target sdk version
Step 5.建立測試專案完成!

使用 Command line 建立 android test project

Step 1. 開啟終端機並使用 android-sdk/tools/android 建立測試專案,建立測試專案的指令如下

android create test-project -p 測試專案路徑 -m 被測專案路徑
-p 代表測試專案路徑
-m 代表被測專案路徑

Note :

  1. 這2個參數都是必須的,因此使用 command line 建立測試專案必須存在被測專案才行。
  2. 被測專案路徑會由被測專案路徑中是否存在 AndroidManifest.xml 來判斷,若被測專案不存在 AndroidManifest.xml 會出現以下錯誤訊息
    Error: No AndroidManifest.xml file found in the main project directory:
  3. 若直接輸入 android 出現 command not found 的錯誤,請參考這篇(將相關指令加入環境變數

以下為範例:

首先建立被測專案 TargetTestProject
可以使用 eclipse 也可以使用 command line,以下使用指令
Step 1. 建立TargetTestProject資料夾,並移動該資料夾中

mkdir TargetTestProject
cd TargetTestProject

Step 2.建立 android project

android create project -p ./ -n TargetTestProject -k com.example.targettestproject -a MainActivity -t android-15
-p path to project directory
-n name of application
-k name of package
-a name of helloworld activity
-t android version

完成後會自動產生一個 MainActivity.java 位置在
TargetTestProject/src/com/example/targettestproject/MainActivity 可以啟動看看是否正常,在已連接裝置或是已啟動 AVD 的狀況下輸入

adb shell am start -n com.example.targettestproject/com.example.targettestproject.MainActivity

應該就可看到該app已啟動完成顯示在裝置上。
再建立測試專案 TestProject

cd ../
mkdir TestProject
cd TestProject
android create test-project -p ./ -m ../TargetTestProject

以上指令除了建立測試專案之外還會幫我們建立一個 test case 為 MainActivityTest.java
該名稱會對應於被測專案的Activity字尾加上Test
位置在 TargetTestProject/src/com/example/targettestproject/MainActivityTest.java
內容如下

package com.example.targettestproject;
import android.test.ActivityInstrumentationTestCase2;
/**
 * This is a simple framework for a test of an Application.  See
 * {@link android.test.ApplicationTestCase ApplicationTestCase} for more information on
 * how to write and extend Application tests.
 * <p/>
 * To run this test, you can type:
 * adb shell am instrument -w \
 * -e class com.example.targettestproject.MainActivityTest \
 * com.example.targettestproject.tests/android.test.InstrumentationTestRunner
 */
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
    public MainActivityTest() {
        super("com.example.targettestproject", MainActivity.class);
    }
}

現在就可以試著啟動測試程式看看。
你可以使用註釋中提到的方式啟動,但是缺點是修改 source code 之後並不會重新編譯以及安裝,如下

 * adb shell am instrument -w \
 * -e class com.example.targettestproject.MainActivityTest \
 * com.example.targettestproject.tests/android.test.InstrumentationTestRunner
 */

不如使用 ant 來的方便,指令比較短也會自動完成需要的工作。

ant uninstall clean debug install test

啟動並完成測試之後,可以看到以下輸出

...
test:
     [echo] Running tests ...
     [exec]
     [exec] Test results for InstrumentationTestRunner=
     [exec] Time: 0.0
     [exec]
     [exec] OK (0 tests)
     [exec]
     [exec]
BUILD SUCCESSFUL
Total time: 16 seconds

成功執行測試,沒有任何測試案例。
下一篇 建立測試案例以及測試 android activity 的模板
 
 

分類
Android

Create Test Case for android project(Step by Step)

官網教學,參考 https://developer.android.com/training/testing.html
這裡為紀錄詳細步驟。
Target Class : RoundAmountSaving也就是要測試的Android app(被測目標),該app能幫你計算本金和,你只需要輸入目前儲蓄金額,年利率,以及儲蓄月數
它就能算出儲蓄多久可得到多少錢。
裡面的元件組成相當簡單,只有幾個EditText,Button,一個簡單的計算公式。

package com.foxx.round_amount_saving;
import com.foxx_round_amount_saving.R;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
public class RoundAmountSaving extends Activity
{
    private static final int PERCENT = 100;
    private static final int MONTH_COUNT = 12;
    private EditText mInputLend;
    private EditText mInputRate;
    private EditText mInputNumber;
    private Button mSubmit;
    private TextView mAmount;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        initLayoutComponents();
    }
    private void initLayoutComponents()
    {
        mInputLend = (EditText) findViewById(R.id.input_lend);
        mInputNumber = (EditText) findViewById(R.id.input_number);
        mInputRate = (EditText) findViewById(R.id.input_rate);
        mSubmit = (Button) findViewById(R.id.submit);
        mSubmit.setOnClickListener(getClickListenerForSubmit());
        mAmount = (TextView) findViewById(R.id.amount);
    }
    private OnClickListener getClickListenerForSubmit()
    {
        return new OnClickListener() {
            @Override
            public void onClick(View v)
            {
                mAmount.setText("" + countAmount());
            }
        };
    }
    private double countAmount()
    {
        int lend = Integer.parseInt(mInputLend.getText().toString());
        double rateOfMonth = Double.parseDouble(mInputRate.getText().toString()) / MONTH_COUNT
                / PERCENT;
        int number = Integer.parseInt(mInputNumber.getText().toString());
        return lend * (Math.pow(rateOfMonth + 1, number));
    }
}

 
開始建立Android Test Project

1. Eclipse -> File -> New -> Other -> type : android test project -> next

 

2. type : RoundAmountSavingTest in project name -> next

 

3. select test target : RoundAmountSaving -> Finish

Note : RoundAmountSaving 是 project Name
完成後會產生android test project,名稱為 RoundAmountSavingTest,其中有設定好的package,接著開始來寫 test case
 

4.建立 junit test case

Eclipse -> File -> New -> Other -> type : junit test case -> next
 

5. 選擇 New Junit 3 test,輸入Name,並選擇Class under test : com.foxx.roundamountsaving.RoundAmountSaving -> Finish

 

6.修改RoundAmountSavingTest.java

由於RoundAmountSaving繼承Activity,為了測試Activity我們必須讓Test Case繼承ActivityInstrumentationTestCase2並呼叫父類別建構式

package com.foxx.roundamountsaving.test;
import android.test.ActivityInstrumentationTestCase2;
import com.foxx.roundamountsaving.RoundAmountSaving;
public class RoundAmountSavingTest extends ActivityInstrumentationTestCase2<RoundAmountSaving>
{
    public RoundAmountSavingTest(){
        super(RoundAmountSaving.class);
    }
}

 
接著override setUp方法,並在方法中設定相關內容,包含測試類別的實體

package com.foxx.roundamountsaving.test;
import android.test.ActivityInstrumentationTestCase2;
import com.foxx.roundamountsaving.RoundAmountSaving;
public class RoundAmountSavingTest extends ActivityInstrumentationTestCase2<RoundAmountSaving>
{
    private RoundAmountSaving mRoundAmountSaving;
    public RoundAmountSavingTest(){
        super(RoundAmountSaving.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        setActivityInitialTouchMode(true);
        mRoundAmountSaving = getActivity();
    }
}

 
可以開始寫測試方法了,為了確保測試資料的完整性,先測試mRoundAmountSaving不可為null

public void testRoundAmountSavingNotNull(){
        assertNotNull(mRoundAmountSaving);
}

 
Run test!!!
加入RoundAmountSaving上的UI元件,測試這些元件的layout或是功能性,
預設情況在38~42行會出現 id cannot be resolved or is not a field 的錯誤,Eclipse 並不會自動修復,我們必須手動import RoundAmountSaving的R(第11行)

package com.foxx.roundamountsaving.test;
import android.test.ActivityInstrumentationTestCase2;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.foxx.roundamountsaving.RoundAmountSaving;
import com.foxx.roundamountsaving.R;
public class RoundAmountSavingTest extends ActivityInstrumentationTestCase2<RoundAmountSaving>
{
    private RoundAmountSaving mRoundAmountSaving;
    private EditText mInputLend;
    private EditText mInputRate;
    private EditText mInputNumber;
    private Button mSubmit;
    private TextView mAmount;
    public RoundAmountSavingTest(){
        super(RoundAmountSaving.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        setActivityInitialTouchMode(true);
        mRoundAmountSaving = getActivity();
        initLayoutComponents();
    }
    public void testRoundAmountSavingNotNull(){
        assertNotNull(mRoundAmountSaving);
    }
    private void initLayoutComponents()
    {
        mInputLend = (EditText) mRoundAmountSaving.findViewById(R.id.input_lend);
        mInputNumber = (EditText) mRoundAmountSaving.findViewById(R.id.input_number);
        mInputRate = (EditText) mRoundAmountSaving.findViewById(R.id.input_rate);
        mSubmit = (Button) mRoundAmountSaving.findViewById(R.id.submit);
        mAmount = (TextView) mRoundAmountSaving.findViewById(R.id.amount);
    }
}

 
加入UI元件不可為null的測試

public void testLayoutComponentsNotNull()
{
        assertNotNull(mInputLend);
        assertNotNull(mInputNumber);
        assertNotNull(mInputRate);
        assertNotNull(mSubmit);
}

 
Run test~
加入測試UI元件layout的測試,assertEquals的第2個參數必須參考RoundAmountSaving的UI xml的設定

public void testAmountLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mAmount.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.WRAP_CONTENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputNumberLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputNumber.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputRateLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputRate.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputLendLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputLend.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testSubmitLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mSubmit.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }

 
Run test~
為了測試UI元件的功能性,加入requestFocusLayoutComponent和setValueToLayoutComponent2個方法,
requestFocusLayoutComponent可讓UI元件取得焦點,取得焦點後再呼叫setValueToLayoutComponent這個方法可以在edittext中設定數值

private void requestFocusLayoutComponent(final View component)
    {
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run()
            {
                component.requestFocus();
            }
        });
    }
private void setValueToLayoutComponent(String value)
    {
        getInstrumentation().waitForIdleSync();
        getInstrumentation().sendStringSync(value);
        getInstrumentation().waitForIdleSync();
    }

 
設定數值到各UI元件

private void inputTestValueInInputNumber()
    {
        requestFocusLayoutComponent(mInputNumber);
        setValueToLayoutComponent("20");
    }
    private void inputTestValueInInputRate()
    {
        requestFocusLayoutComponent(mInputRate);
        setValueToLayoutComponent("10");
    }
    private void inputTestValueInInputLend()
    {
        requestFocusLayoutComponent(mInputLend);
        setValueToLayoutComponent("100");
    }

 
 
最後加入測試計算的正確性,expectedCountAmountResult 為預期的計算結果,先設定數值(inutTestValueInInputXXX)到各UI元件中
再呼叫TouchUtils.clickView模擬mSubmit被按下,最後從mAmount取得計算結果和expectedCountAmountResult比較是否相等
相等的話代表算法正確

public void testCountAmount()
    {
        final double expectedCountAmountResult = 118.05448347663095;
        inputTestValueInInputLend();
        inputTestValueInInputRate();
        inputTestValueInInputNumber();
        TouchUtils.clickView(this, mSubmit);
        String result = (String) mAmount.getText();
        assertEquals(result, "" + expectedCountAmountResult);
    }

 
Run test~
其實在測試算法還可以加入更多的測試條件,如邊界值,特殊值等等
final RoundAmountSavingTest.java

package com.foxx.roundamountsaving.test;
import android.test.ActivityInstrumentationTestCase2;
import android.test.TouchUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.foxx.roundamountsaving.RoundAmountSaving;
import com.foxx.roundamountsaving.R;
public class RoundAmountSavingTest extends ActivityInstrumentationTestCase2<RoundAmountSaving>
{
    private RoundAmountSaving mRoundAmountSaving;
    private EditText mInputLend;
    private EditText mInputRate;
    private EditText mInputNumber;
    private Button mSubmit;
    private TextView mAmount;
    public RoundAmountSavingTest(){
        super(RoundAmountSaving.class);
    }
    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        setActivityInitialTouchMode(true);
        mRoundAmountSaving = getActivity();
        initLayoutComponents();
    }
    public void testRoundAmountSavingNotNull(){
        assertNotNull(mRoundAmountSaving);
    }
    public void testLayoutComponentsNotNull()
    {
        assertNotNull(mInputLend);
        assertNotNull(mInputNumber);
        assertNotNull(mInputRate);
        assertNotNull(mSubmit);
    }
    private void initLayoutComponents()
    {
        mInputLend = (EditText) mRoundAmountSaving.findViewById(R.id.input_lend);
        mInputNumber = (EditText) mRoundAmountSaving.findViewById(R.id.input_number);
        mInputRate = (EditText) mRoundAmountSaving.findViewById(R.id.input_rate);
        mSubmit = (Button) mRoundAmountSaving.findViewById(R.id.submit);
        mAmount = (TextView) mRoundAmountSaving.findViewById(R.id.amount);
    }
    private void requestFocusLayoutComponent(final View component)
    {
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run()
            {
                component.requestFocus();
            }
        });
    }
    private void setValueToLayoutComponent(String value)
    {
        getInstrumentation().waitForIdleSync();
        getInstrumentation().sendStringSync(value);
        getInstrumentation().waitForIdleSync();
    }
    public void testCountAmount()
    {
        final double expectedCountAmountResult = 118.05448347663095;
        inputTestValueInInputLend();
        inputTestValueInInputRate();
        inputTestValueInInputNumber();
        TouchUtils.clickView(this, mSubmit);
        String result = (String) mAmount.getText();
        assertEquals(result, "" + expectedCountAmountResult);
    }
    private void inputTestValueInInputNumber()
    {
        requestFocusLayoutComponent(mInputNumber);
        setValueToLayoutComponent("20");
    }
    private void inputTestValueInInputRate()
    {
        requestFocusLayoutComponent(mInputRate);
        setValueToLayoutComponent("10");
    }
    private void inputTestValueInInputLend()
    {
        requestFocusLayoutComponent(mInputLend);
        setValueToLayoutComponent("100");
    }
    public void testAmountLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mAmount.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.WRAP_CONTENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputNumberLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputNumber.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputRateLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputRate.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testInputLendLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mInputLend.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
    public void testSubmitLayoutValid()
    {
        final ViewGroup.LayoutParams layoutParams = mSubmit.getLayoutParams();
        assertNotNull(layoutParams);
        assertEquals(layoutParams.width, WindowManager.LayoutParams.MATCH_PARENT);
        assertEquals(layoutParams.height, WindowManager.LayoutParams.WRAP_CONTENT);
    }
}