作成したアプリケーションは、どのように動作確認していますか? 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> { } |
- コンストラクタ
- テストを始める前に処理を行いたい場合(データベースのオープン、変数の初期化など)は、setUp()メソッドをオーバーライドし、そこに処理を書く。
- テスト終了後に処理を行いたい場合(データベースのクローズ、メモリの解放など)は、tearDown()メソッドをオーバーライドし、そこに処理を書く。
- テストケースは、「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スレッドで処理を行う必要があります。テストケース内では、以下のコードで行うことが可能です。
@Override
public void run() {
// TODO Auto-generated method stub
//ここにUI操作を書く
}
});
getInstrumentation().waitForIdleSync();
なお、UIスレッドに処理を依頼した場合は、UIスレッドの処理が終了するのを待つ必要があります(getInstrumentation().waitForIdleSync()が該当します)。これがないと、UI処理が終了する前に、テスト工程が次々走ってしまうことになります(画面と内部データの整合性がとれなくなる)。
次に、キー入力を行う方法ですが、sendKeys()で、UIスレッドに対してキーイベントを発生させるメソッドが用意されており、引数にKeyEventを与えると、そのキー入力がイベントとして発生します。例えば、「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()); } } |
[参考]
- 有限会社シーリス 代表 有山 圭二氏 「第1回名古屋Android勉強会」Androidテスト入門 ハンズオン資料「HansOn-JUnitandMonkey-20110115.pdf」
ピンバック: Tweets that mention AndroidアプリケーションをJUnitでテストする | Android開発メモ -- Topsy.com
ピンバック: AndroidアプリケーションをJUnitでテストする | Android開発メモ | Android情報局
ピンバック: AndroidアプリケーションをJUnitでテストする | Android開発メモ | さいと おぶ ちゅどん
ピンバック: 初心者なアタシがAndroidでアプリを無料で開発するとき役に勃つ必要最小限な5つだけのアレ | 新! #android ファッション通信 Σ(^A^;)
ピンバック: 【Android】URLメモ【メモ】 : dreamroad
ためになりました。
ありがとうございます。
勉強になりました!
ありがとうございます!