分類
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 Uncategorized

在 Eclipse 使用 Android Manager 更新 android sdk 25之後無法使用 adb 問題

開啟 eclipse 啟動 android manager 並更新到 android adk 25.0.2 之後,使用 adb 會出現以下錯誤訊息

platform-tools ./adb devices
zsh: 可執行檔格式錯誤: ./adb

root cause:unknow
workaround : 到 android repository 下載並解壓縮 platform-tools.zip (經過測試只有platform-tools_r20-linux.zip 可用),替換掉原本無法使用的platform-tools 資料夾即可。
 

分類
Android

使用 gcc 編譯 .o 以及 .so 檔

首先確定已經存在 .c 檔(這裡以 hello-jni 為範例,在預設的情況下,已經存在/project_root_path/jni/hello-jni.c。

Step 1.打開Terminal並移動到專案根目錄的 jni 資料夾(/project_root_path/jni/)。

 

Step 2.編譯 .o 檔

輸入

gcc -Wall -fPIC -c hello-jni.c -I /usr/lib/jvm/java-7-oracle/include/linux/ -I /usr/lib/jvm/java-7-oracle/include/

Note:
-Wall:產生所有警告訊息。
-fPIC:表示編譯後為位置獨立的代碼。
-c:產生.o檔案。
hello-jni.c:代表要編譯的檔案,若有多個檔案必須使用空格分開,也可以指定路徑。
-I /path_of_directory:指定額外.h檔的搜索路徑(/path_of_directory),這是用在編譯的檔案(.c)中出現
#include “file” 的時候 , gcc/g++會在目前目錄尋找.h檔,如果沒有找到,會回到預設的.h目錄尋找,如果使用-I指定目錄,gcc 會從指定的目錄尋找,然後順著一般的順序。
因為hello-jni.c 有 #include <jni.h>,jni.h的位置即為 /usr/lib/jvm/java-7-oracle/include/
編譯完成後會產生 hello-jni.o 檔案
 

Step 3.編譯 .so 檔

輸入

gcc -Wall -rdynamic -shared -o libhello-jni.so hello-jni.o

完成後會產生 libhello-jni.so 檔
 

以上 Step 2 和 Step 3 可以使用一句指令完成,省去了產生.o檔的步驟,e.g.

gcc hello-jni.c -fPIC -shared -I /usr/lib/jvm/java-7-oracle/include/linux/ -I /usr/lib/jvm/java-7-oracle/include/ -lm -o libhello-jni.so

 
 

分類
Android

Android.mk 和 Application.mk

Android.mk 為 android ndk project 必備的描述檔,為 GNU Makefile 格式。

它的位置必須在jni目錄中,主要是說明如何build source 和 shared libraries。
在 /ndk的根目錄/build/core/ 底下還有許多mk檔可以參考。
以下為 HelloJni(/android-ndk/samples/HelloJni) 的 Android.mk

# Copyright (C) 2009 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.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)

首先#開頭的都為註解。
第15行規定為每個 Android.mk 起始行,不可替換成別的內容,其中 my-dir 為指出目前的路徑,也就是jni目錄的路徑。
第17行 CLEAR_VARS 會連結到 /ndk的根目錄/build/core/clear-vars.mk。
目的是清除所有除了LOCAL_PATH以外的 LOCAL_XXX 的變數,e.g. LOCAL_MODULE , LOCAL_SRC_FILES 等等。
Android.mk 在每一次的執行過程中可能會建立多個module,因為以LOCAL_ 開頭的都是全域變數,為了避免module之間的衝突,必須清除掉。
第19行為命名 module 的名稱,系統自動會加上前綴lib 以及後綴.so,最後會產生一個共享庫文件.so ,並名為 libhello-jni.so
第20行為用來建立這個module的來源檔,若有多個檔案,必須以空格分開。
第22行 BUILD_SHARED_LIBRARY 會連結到 /ndk的根目錄/build/core/build-shared-library.mk。
目的是將來源檔案轉換為共享庫文件的動作,其完整動作會描述在build-shared-library.mk 。
 

如何藉由 Android.mk 編譯 c/c++檔案:

1.開啟 Terminal,並移動到專案根目錄中。
2.呼叫 ndk-build,Terminal 輸入 ndk-build 絕對路徑(/path_of_android_ndk/ndk-build)。
3.編譯成功輸出

Android NDK: WARNING: APP_PLATFORM android-8 is larger than android:minSdkVersion 3 in ./AndroidManifest.xml
[arm64-v8a] Gdbserver      : [aarch64-linux-android-4.9] libs/arm64-v8a/gdbserver
[arm64-v8a] Gdbsetup       : libs/arm64-v8a/gdb.setup
[x86_64] Gdbserver      : [x86_64-4.9] libs/x86_64/gdbserver
[x86_64] Gdbsetup       : libs/x86_64/gdb.setup
[mips64] Gdbserver      : [mips64el-linux-android-4.9] libs/mips64/gdbserver
[mips64] Gdbsetup       : libs/mips64/gdb.setup
[armeabi-v7a] Gdbserver      : [arm-linux-androideabi-4.8] libs/armeabi-v7a/gdbserver
[armeabi-v7a] Gdbsetup       : libs/armeabi-v7a/gdb.setup
[armeabi] Gdbserver      : [arm-linux-androideabi-4.8] libs/armeabi/gdbserver
[armeabi] Gdbsetup       : libs/armeabi/gdb.setup
[x86] Gdbserver      : [x86-4.8] libs/x86/gdbserver
[x86] Gdbsetup       : libs/x86/gdb.setup
[mips] Gdbserver      : [mipsel-linux-android-4.8] libs/mips/gdbserver
[mips] Gdbsetup       : libs/mips/gdb.setup
[arm64-v8a] Install        : libhello-jni.so => libs/arm64-v8a/libhello-jni.so
[x86_64] Install        : libhello-jni.so => libs/x86_64/libhello-jni.so
[mips64] Install        : libhello-jni.so => libs/mips64/libhello-jni.so
[armeabi-v7a] Install        : libhello-jni.so => libs/armeabi-v7a/libhello-jni.so
[armeabi] Install        : libhello-jni.so => libs/armeabi/libhello-jni.so
[x86] Install        : libhello-jni.so => libs/x86/libhello-jni.so
[mips] Install        : libhello-jni.so => libs/mips/libhello-jni.so
➜

編譯成功後在專案根目錄會多出 obj , libs 資料夾,.so會產生在libs資料夾內。
 
進階用法:
1.建立多個共享庫文件。

LOCAL_PATH := $(call my-dir)
#第1個模組
include $(CLEAR_VARS)
LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)
#第2個模組
include $(CLEAR_VARS)
LOCAL_MODULE    := module2
LOCAL_SRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)

 
2. 建立靜態函式庫。
可將第3方的模組轉換為靜態函式庫,並加到原始函式庫上

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := third_party_lib
LOCAL_SRC_FILES := third.c
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE    := module
LOCAL_SRC_FILES := module.c
LOCAL_STATIC_LIBRARIES := third_party_lib
include $(BUILD_SHARED_LIBRARY)

 
3.多個模組使用同一個靜態函式庫

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := third_party_lib
LOCAL_SRC_FILES := third.c
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE    := module1
LOCAL_SRC_FILES := module.c
LOCAL_STATIC_LIBRARIES := third_party_lib
include $(BUILD_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE    := module2
LOCAL_SRC_FILES := module2.c
LOCAL_STATIC_LIBRARIES := third_party_lib
include $(BUILD_SHARED_LIBRARY)

參考官網:https://developer.android.com/ndk/guides/android_mk.html#over

Application.mk 為描述應用程序需要哪些模組,也定義模組間的通用變數,必須位於 jni 目錄。

以下為 HelloJni 的 Application.mk

APP_ABI := all

APP_ABI 表示平台,而 all 代表為所有支援的平台建立二建制文件。
參考官網:https://developer.android.com/ndk/guides/application_mk.html
 
 

分類
Android

android ndk 主要元件以及結構

android ndk 主要的元件

  • 交叉編譯器(cross compiler):ARM , x86 , MIPS
  • 構建系統
  • Java native interface .h 檔案
  • C 函式庫
  • Math 函式庫
  • POSIX 執行緒
  • 小型的 C++ 函式庫
  • ZLib函式庫
  • 動態連結函式庫
  • Android Log 函式庫
  • Android NDK native APIs
  • OpenGL ES 3D
  • OpenSL ES
  • OpenMAX AL

 

android ndk 結構中一些比較重要的目錄以及腳本

  • ndk-build:為 ndk 構件系統的起始點。
  • ndk-gdb:可以使用 GNU Debugger 測試原生元件。
  • ndk-stack:原生組件崩潰時的追蹤堆棧。
  • build 目錄:包含所有 ndk 的 module。
  • platforms 目錄:包含不同 android 版本的 .h 檔以及 library
  • samples 目錄:範例
  • sources 目錄:共享 modules
  • toolchains 目錄:交叉編譯器,目前支援 ARM , X86 , MIPS

 

android ndk 專案重要的目錄

  • jni 目錄:為NDK項目的目錄。包含原生組件的 source code 以及 android.mk 檔。
  • libs 目錄:在 android ndk 建立過程中產生,包含指定的平台的獨立子目錄 e.g. ARM的armeabi。本目錄會打包在APK中。
  • obj 目錄:包含編譯 source code 之後所產生的目標文件。
分類
Android Uncategorized

查表法(表驅動法)(Table-Driven-Methods )

查表法(Table-Driven-Method)主要是藉由表格的維度和對應的數值來替代冗長的判斷式(if-else or switch)
查表法根據不同的查表方式,可分為三種:
直接存取(direct access):可從表格中直接讀取數值。
索引存取(indexed access):先透過索引取出key值,再從key值取出數值。
階梯存取(stair-step access):對於不同的數值範圍有效。
 


 
直接存取:
如果要查詢月份對應的天數,最直接的寫法 e.g.,

    private int getDaysOfMonth(int month)
    {
        if (month == 1) {
            return 31;
        } else if (month == 2) {
            return 28;
        } else if (month == 3) {
            return 31;
        } else if (month == 4) {
            return 30;
        } else if (month == 5) {
            return 31;
        } else if (month == 6) {
            return 30;
        } else if (month == 7) {
            return 31;
        } else if (month == 8) {
            return 31;
        } else if (month == 9) {
            return 30;
        } else if (month == 10) {
            return 31;
        } else if (month == 11) {
            return 30;
        } else if (month == 12) {
            return 31;
        } else {
            throw new IllegalArgumentException();
        }
    }

可以藉由以下表格來取代

private static final int[] DAYS_OF_MONTH = {
            31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};

取值如下,第7行為使用查表法的動作。

    @Test
    public void testGetDayOfMonth()
    {
        int[] months = {1,2,3,4,5,6,7,8,9,10,11,12};
        for(int month : months){
            int resultBySwitch = getDaysOfMonth(month);
            int resultByTable = DAYS_OF_MONTH[month - 1];
            assertEquals(resultBySwitch, resultByTable);
        }
    }

 


 
索引存取:
如果條件判斷式的變數型態不是int , 就無法使用陣列來建表,必須改用Map,而 Map 的 key 值即是索引值。e.g.

    private int getDayOfMonthByName(String nameOfMonth)
    {
        if (nameOfMonth.equals("Jan")) {
            return 31;
        } else if (nameOfMonth.equals("Feb")) {
            return 28;
        } else if (nameOfMonth.equals("Mar")) {
            return 31;
        } else if (nameOfMonth.equals("Apr")) {
            return 30;
        } else if (nameOfMonth.equals("May")) {
            return 31;
        } else if (nameOfMonth.equals("Jun")) {
            return 30;
        } else if (nameOfMonth.equals("Jul")) {
            return 31;
        } else if (nameOfMonth.equals("Aug")) {
            return 31;
        } else if (nameOfMonth.equals("Sep")) {
            return 30;
        } else if (nameOfMonth.equals("Oct")) {
            return 31;
        } else if (nameOfMonth.equals("Nov")) {
            return 30;
        } else if (nameOfMonth.equals("Dec")) {
            return 31;
        } else {
            throw new IllegalArgumentException();
        }
    }

使用Map取值,第1到10行為初始化Map , 第14行為使用Map取值。

        Map<String, Integer> DAY_OF_MONTH = new HashMap<String,Integer>();
        String[] nameOfMonth = new String[] {
                "Jan", "Feb", "Mar", "Apr", "May","Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
        };
        Integer[] numberDayOfMonth = {
                31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
        };
        for (int i = 0; i < nameOfMonth.length; ++i) {
            DAY_OF_MONTH.put(nameOfMonth[i], numberDayOfMonth[i]);
        }
        for(String name : nameOfMonth){
            int resultByMethod = getDayOfMonthByName(name);
            int resultByMap = DAY_OF_MONTH.get(name);
            assertEquals(resultByMethod, resultByMap);
        }

 


 
階梯存取:
假設現在要對不同分數給予等級的判斷, e.g.

    private String getNameOfGradeByMethod(int score)
    {
        if (score >= 90) {
            return "A";
        } else if (score >= 80 && score < 90) {
            return "B";
        } else if (score >= 70 && score < 80) {
            return "C";
        } else if (score >= 60 && score < 70) {
            return "D";
        } else if (score < 60) {
            return "F";
        } else {
            throw new IllegalArgumentException();
        }
    }

需要建立2個對應的table , 以及求值的函式(getNameOfGradeByTable)。 e.g.

    private static final String NAME_OF_GRADE_LEVEL[] = {
            "A", "B", "C", "D", "F"
    };
    private static final int NUMBER_OF_GRADE_LEVEL[] = {
            90, 80, 70, 60
    };
    private String getNameOfGradeByTable(int score)
    {
        int gradeLevel = 0;
        while (NAME_OF_GRADE_LEVEL[gradeLevel] != NAME_OF_GRADE_LEVEL[NAME_OF_GRADE_LEVEL.length - 1]) {
            if (score < NUMBER_OF_GRADE_LEVEL[gradeLevel]) {
                ++gradeLevel;
            } else {
                break;
            }
        }
        return NAME_OF_GRADE_LEVEL[gradeLevel];
    }

雖然整體看起來比判斷式來的長,但若是接下來需要增加更多的判斷如 60, 50, 40, 30, 20, 10。
對於查表法只要在 table 加入相對應數值即可。
也適合用於沒有規則變化的求值。


 
事實上查表法無法去除掉原判斷式的邏輯,它只是把邏輯搬移到表中。表格的複雜度會跟著原判斷式的邏輯成正比。
另外也需要提供空間儲存表格。
查表法可以減少程式碼的長度,但無法簡化程式碼。
對可讀性的幫助並不大,反而需要完全理解表格才能修改原功能或是增加新功能。
因此最適合的情況為冗長但判斷邏輯簡單的條件式。
更複雜的查表法範例 ref
 

分類
Android

Android Error : Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser;

Description:

引入外部 Library , 啟動 android app 時出現。

[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser$1;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser$2;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser$3;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Browser$ServiceTableModel;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Main;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Main$SampleListener;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Lcom/strangeberry/jmdns/tools/Responder;
[2016-03-04 10:20:58 - SmackForXep0174] Dx Uncaught translation error: java.lang.IllegalArgumentException: already added: Ljavax/jmdns/package-info;
[2016-03-04 10:20:58 - Dex Loader] Unable to execute dex: Too many errors
[2016-03-04 10:20:58 - SmackForXep0174] Conversion to Dalvik format failed: Unable to execute dex: Too many errors

即使把所有變動都移除,讓專案回復未引用 Library 再重新 build,錯誤訊息一樣產生。
必須把 eclipse 重新啟動,錯誤訊息才會消失。
 

Solution:

從錯誤訊息去尋找相關線索。
一連串的錯誤訊息提示都和 jmdns有關,在app中 也引用了 jmdns Library。
StackOverFlow 提示 ref: http://stackoverflow.com/questions/11697979/android-build-with-jmdns-fails
看起來是SourceForge 上檔案有問題,根據 StackOverFlow 提示改換 maven 上的 Library
app 成功啟動!!

分類
Android Uncategorized

eclipse 設定 code style

1.set customize code style

Window -> Preferences -> java -> code style -> Formatter -> import -> select your own rule xml