AndroidアプリケーションをJUnitでテストする

作成したアプリケーションは、どのように動作確認していますか? Androidには、Javaの単体テストのフレームワークである「JUnit」を使用することができます。今回は、その使用方法について説明します。

<1>アプリケーションの仕様を決める

まず、作成するアプリケーションの仕様を決定しないといけませんが、今回は下のようなAndroidアプリケーションを作成します。

  • 画面の一番上にEditTextを貼り付ける。
  • EditTextの下に、TextViewを貼り付ける。
  • EditText、TextViewの初期値は空文字。
  • TextViewの下には「コピー」という文字が入ったボタンを貼り付ける。
  • 「コピー」ボタンは、EditTextに文字が入力されていない場合は、ボタンを押下することはできない(無効になっている)。
  • 「コピー」ボタンを押下すると、EditTextの内容がTextViewにコピーされる。

<2>プロジェクトの作成

Androidのプロジェクトを作成しますが、プロジェクトを作成した時に、通常は「Finish」ボタンで終了しますが、テストプロジェクトも同時に作成するため、ここでは、「Next」をクリックします。 今回は、以下のプロジェクトを作成しました。

  • Project Name:TestProject
  • Build Taget:Android 1.6(API Level 4)
  • Application Name:TestProject
  • Package Name:jp.main.itinfo.tan.testproject
  • Create Activity:(チェックを入れる)  MainActivity
  • Min SDK Version:4


次の画面では、「Contents」のチェックボックスを「ON」にして、ウィンドウ最下部にある「Finish」ボタンをクリックします。

Androidアプリケーション用のプロジェクト「TestProject」と、テスト用プロジェクト「TestProjectTest」が作成されます。

<3>Androidアプリケーションを作成する

<1>の仕様にあったAndroidアプリケーションを作成します。「TestProject」プロジェクトで作成していきます。ここではAndroidでのコードの説明は行いません(また別の機会に行う予定です)。

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package jp.main.itinfo.tan.testproject;

/**
 * ユニットテスト用に作成したAndroidアプリケーション
 */

import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends Activity {
   
    private EditText    mEditTextFrom;  //エディットテキスト
    private TextView    mTextViewTo;    //テキストビュー
    private Button      mCopyButton;    //コピーボタン
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        mEditTextFrom = (EditText) findViewById(R.id.EditTextFrom);
        mTextViewTo = (TextView) findViewById(R.id.EditTextTo);
        mCopyButton = (Button) findViewById(R.id.ButtonCopy);

        mCopyButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mTextViewTo.setText(mEditTextFrom.getText().toString());
            }
        });
       
       
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if(mEditTextFrom.getText().toString().length() == 0) {
            mCopyButton.setEnabled(false);
        } else {
            mCopyButton.setEnabled(true);
        }
       
        return super.onKeyUp(keyCode, event);
    }
}

main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
>
    <EditText
        android:id="@+id/EditTextFrom"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text=""
    />
    <TextView
        android:id="@+id/EditTextTo"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text=""
    />
    <Button
        android:id="@+id/ButtonCopy"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="コピー"
        android:enabled="false"
    />
</LinearLayout>

<4>テストケース作成

「TestProjectTest」プロジェクトに、テストケースを作成します。テストケース用の新規クラスを作成しますが、以下のパラメータを使用します。

  • Package:jp.main.itinfo.tan.testproject.test
  • Name:TestCase
  • Superclass:ActivityInstrumentationTestCase2<T>

すると、自動的にテストケースのクラスが作成されます。

1
2
3
4
5
package jp.main.itinfo.tan.testproject.test;

public class TestCase extends ActivityInstrumentationTestCase2<T> {

}
続いて、<1>の仕様に基づいたテストケースを書いていきます。テストケースは全てメソッドで表現しますが、決まりごとみたいなものがあります。
  1. コンストラクタ
  2. テストを始める前に処理を行いたい場合(データベースのオープン、変数の初期化など)は、setUp()メソッドをオーバーライドし、そこに処理を書く。
  3. テスト終了後に処理を行いたい場合(データベースのクローズ、メモリの解放など)は、tearDown()メソッドをオーバーライドし、そこに処理を書く。
  4. テストケースは、「test」から始まるメソッド名で書く。

まず、コンストラクタを定義しますが、その前にエラーを訂正します。ActivityInstrumentationTestCase2<T>でエラーが発生していますので、「T」を「テストを行いたいクラス名(今回はMainActivity)に置き換えます。また、「TestCase」でもエラーが発生していますので、マウスカーソルを合わせ、「Add constructor ‘TestCase(String,Class<MainActivity>)’」を選択します。


修正が完了したコードは以下となります。

1
2
3
4
5
6
7
8
9
10
package jp.main.itinfo.tan.testproject.test;

import jp.main.itinfo.tan.testproject.MainActivity;
import android.test.ActivityInstrumentationTestCase2;
public class TestCase extends ActivityInstrumentationTestCase2<MainActivity> {
    public TestCase(String pkg, Class<MainActivity> activityClass) {
        super(pkg, activityClass);
        // TODO Auto-generated constructor stub
    }
}

コンストラクタが1つ追加されましたが、もうひとつ「引数なし」のコンストラクタを追加します。コンストラクタ内では、親クラスのコンストラクタを呼び出しますが、第1引数は、テストを行いたいパッケージ名、第2引数はテストを行いたいクラス名を指定します。

1
2
3
public TestCase() {
    super("jp.main.itinfo.tan.testproject", MainActivity.class);
}

続いて、今回は「エディットテキスト」「テキストビュー」「ボタン」の計3つのオブジェクトがありますので、それを定義し、リソースと紐付けします。これらの処理は、テスト前に行ったほうがよいので、setUp()に記入します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private EditText    mEditTextFrom;
private TextView    mTextViewTo;
private Button  mButtonCopy;
private Activity    mActivity;

@Override
protected void setUp() throws Exception {
    // TODO Auto-generated method stub
    super.setUp();

    //アクティビティを取得する
    mActivity = getActivity();
    //各オブジェクトとリソースを紐付けする
    mEditTextFrom = (EditText) mActivity.findViewById(R.id.EditTextFrom);
    mTextViewTo = (TextView) mActivity.findViewById(R.id.EditTextTo);
    mButtonCopy = (Button) mActivity.findViewById(R.id.ButtonCopy);
}

ようやく、テストケースの作成です。<1>の仕様に合わせてコーディングしていきますが、まずメソッド名を決めます。先頭が「test」と付くように決めてください。今回は「testInitEditText」としました。

次に、

  • EditTextの初期値は空文字

という仕様なので、次のように記入します。

1
2
3
public void testInitEditText() {
    assertTrue(mEditTextFrom.getText().toString().length() == 0);
}

ここで、「assertTrue()」が出てきましたが、これは「引数の中がtrueかどうかを判定する」メソッドです。もし、引数の中がfalseの場合は、テスト失敗を意味します。なお、引数の中を判定するメソッドは各種用意されています。

  • assertEquals(arg1, arg2) → arg1とarg2が同じ値かどうか?
  • assertFalse(boolean) → 引数がfalseかどうか?
  • assertNotNull(Object) → 引数のオブジェクトがnullではないか?
  • assertNull(Object) → 引数のオブジェクトがnullか?
  • assertSame(Object1, Object2) → Object1とObject2が同じオブジェクトを参照しているか?
  • assertNotSame(Object1, Object2) → Object1とObject2が違うオブジェクトを参照しているか?
  • fail() → 強制的にテスト失敗にする

これでテストケースのひとつが完成したので、早速実行してみましょう。実行するには、「テストケース」プロジェクトを右クリックし、「Run As」→「Android JUnit Test」をクリックします。

エミュレータ、または実機でテストが実行され、Eclipseにテスト結果が表示されます。

赤く括られた箇所が「緑色」になっていれば「テスト成功」、「赤色」になっていれば「テスト失敗」です。「赤色」の場合は、仕様と実装が異なることを意味していますので、テストケース、アプリケーションの両方を確認してください。また、「Errors」の数字が0ではない場合は、テストケース内でエラー(例外エラーなど)が発生していますので、テストケースの見直しが必要です。

テキストビューとボタンについても、同様にテストを行います。テキストビューは初期値として空文字、ボタンは文字列が「コピー」であることと、ボタンが押せない仕様です。

1
2
3
4
5
6
7
8
public void testInitTextView() {
    assertTrue(mTextViewTo.getText().toString().length() == 0);
}

public void testInitButton() {
    assertEquals(0, mButtonCopy.getText().toString().compareTo("コピー"));
    assertFalse(mButtonCopy.isEnabled());
}

続いて、「EditTextに文字が入力された場合、ボタンが有効になる」ことをテストします。みなさんは、実機でEditTextに文字を入力する場合はどうしますか?おそらく、

  • EditTextをタップし、フォーカスを移す

ことをしているでしょう。テストでも、同じような操作を端末に送る必要があります。オブジェクトに対してフォーカスを移すには、各オブジェクトに対してrequestFocus()を呼ぶ必要があります。しかし、Androidの基本動作として、UI操作はUIスレッドで行わなければならないため、そのままrequestFocus()をしても失敗してしまいます(例外エラーが発生します)。

そこで、UI操作を行うためには、UIスレッドで処理を行う必要があります。テストケース内では、以下のコードで行うことが可能です。

getActivity().runOnUiThread(new Runnable() {
    @Override
    public void run() {
        // TODO Auto-generated method stub
        //ここにUI操作を書く
    }
});
getInstrumentation().waitForIdleSync();

なお、UIスレッドに処理を依頼した場合は、UIスレッドの処理が終了するのを待つ必要があります(getInstrumentation().waitForIdleSync()が該当します)。これがないと、UI処理が終了する前に、テスト工程が次々走ってしまうことになります(画面と内部データの整合性がとれなくなる)。

次に、キー入力を行う方法ですが、sendKeys()で、UIスレッドに対してキーイベントを発生させるメソッドが用意されており、引数にKeyEventを与えると、そのキー入力がイベントとして発生します。例えば、「1」という数字を入力する場合は、

sendKeys(KeyEvent.KEYCODE_1);

となります。その他、ボタンタップのイベントは、ボタンオブジェクトに対して、performClick()メソッドを呼び出すことで、イベントを発生させることができます。

以上の内容から、テストケースの全てのソースは以下の通りです。かなり長いソースになりますが、内容は非常に簡単です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package jp.main.itinfo.tan.testproject.test;

import jp.main.itinfo.tan.testproject.MainActivity;
import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;
import android.view.KeyEvent;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import jp.main.itinfo.tan.testproject.R;

public class TestCase extends ActivityInstrumentationTestCase2<MainActivity> {

    public TestCase(String pkg, Class<MainActivity> activityClass) {
        super(pkg, activityClass);
        // TODO Auto-generated constructor stub
    }

    public TestCase() {
        super("jp.main.itinfo.tan.testproject", MainActivity.class);
    }
   
    private EditText    mEditTextFrom;
    private TextView    mTextViewTo;
    private Button      mButtonCopy;
    private Activity    mActivity;
   
    @Override
    protected void setUp() throws Exception {
        // TODO Auto-generated method stub
        super.setUp();
       
        //アクティビティを取得する
        mActivity = getActivity();
        //各オブジェクトとリソースを紐付けする
        mEditTextFrom = (EditText) mActivity.findViewById(R.id.EditTextFrom);
        mTextViewTo = (TextView) mActivity.findViewById(R.id.EditTextTo);
        mButtonCopy = (Button) mActivity.findViewById(R.id.ButtonCopy);
    }
   
    public void testInitEditText() {
        //EditTextが空文字かどうかを判定する
        assertTrue(mEditTextFrom.getText().toString().length() == 0);
    }
   
    public void testInitTextView() {
        //TextViewが空文字かどうかを判定する
        assertTrue(mTextViewTo.getText().toString().length() == 0);
    }
   
    public void testInitButton() {
        //Button文字列が「コピー」かどうかを判定する
        assertEquals("コピー", mButtonCopy.getText().toString());
        //Buttonが無効(タップ不可)かどうかを判定する
        assertFalse(mButtonCopy.isEnabled());
    }
   

    public void testInputEditText() {
        //EditTextにフォーカスを移す
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                mEditTextFrom.requestFocus();
            }
        });
        //UIスレッドが終了するまで待つ
        getInstrumentation().waitForIdleSync();
       
        //EditTextに「1」を入力する
        sendKeys(KeyEvent.KEYCODE_1);
        //UIスレッドが終了するまで待つ
        getInstrumentation().waitForIdleSync();
       
        //ボタンが有効(タップ可)かどうかを判定する
        assertTrue(mButtonCopy.isEnabled());

        //ボタンにフォーカスを移す
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                mButtonCopy.requestFocus();
            }
        });
        //UIスレッドが終了するまで待つ
        getInstrumentation().waitForIdleSync();
       
        //ボタンをタップする
        mActivity.runOnUiThread(new Runnable() {
           
            @Override
            public void run() {
                // TODO Auto-generated method stub
                mButtonCopy.performClick();
            }
        });
        //UIスレッドが終了するまで待つ
        getInstrumentation().waitForIdleSync();
       
        //TextEditの内容がTextViewにコピーされているかを確認する
        assertEquals(mEditTextFrom.getText().toString(), mTextViewTo.getText().toString());

        //EditTextにフォーカスを移す
        mActivity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                mEditTextFrom.requestFocus();
            }
        });
        //UIスレッドが終了するまで待つ
        getInstrumentation().waitForIdleSync();
       
        //EditTextに「BackSpace」を入力する
        sendKeys(KeyEvent.KEYCODE_DEL);
        //UIスレッドが終了するまで待つ
        getInstrumentation().waitForIdleSync();
       
        //ボタンが無効(タップ不可)かどうかを判定する
        assertFalse(mButtonCopy.isEnabled());
    }
}

[参考]

カテゴリー: Android パーマリンク

AndroidアプリケーションをJUnitでテストする への7件のフィードバック

  1. ピンバック: Tweets that mention AndroidアプリケーションをJUnitでテストする | Android開発メモ -- Topsy.com

  2. ピンバック: AndroidアプリケーションをJUnitでテストする | Android開発メモ | Android情報局

  3. ピンバック: AndroidアプリケーションをJUnitでテストする | Android開発メモ | さいと おぶ ちゅどん

  4. ピンバック: 初心者なアタシがAndroidでアプリを無料で開発するとき役に勃つ必要最小限な5つだけのアレ | 新! #android ファッション通信 Σ(^A^;)

  5. ピンバック: 【Android】URLメモ【メモ】 : dreamroad

  6. Linoal のコメント:

    ためになりました。
    ありがとうございます。

  7. 師子乃 のコメント:

    勉強になりました!
    ありがとうございます!

師子乃 へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です