2011年6月13日月曜日

簡単なActivityInstrumentationTestCase2の書き方

Androidのプログラムの作成方法については、いろいろなサイトで有用な情報が得られます。
しかし、テストとなるとあまり情報が多くありません。

というわけで、
簡単なActivityInstrumentationTestCase2<T extends Activity>の書き方を
記事にしました。

今回のテスト対象のアプリケーションはこんな感じです。
概要
  • EditViewButtonTextViewで構成されている。
  • EditViewに文字を入力して、Buttonを押すと、TextViewに「Hello, + [EditViewの文字]」が表示される。



まあ、百聞は一見にしかずなので、画面イメージ出しましょう。
これが最初の画面です。


そして、これに文字を入れて、ボタンを押すとこうなります。






















では、早速テストを書きましょう。
まずは、コンストラクターとsetUp()から。


class HelloAndroidTest
        extends ActivityInstrumentationTestCase2<HelloActivity>{

    private Activity activity;

    private Instrumentation instrumentation;

    public HelloAndroidTest(){
        super(HelloActivity.class);
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();
        activity = getActivity();
        instrumentaion = getInstrumentation();
        setActivityInitialTouchMode(false);
    }        
}


まず、ここのポイントは
  • コンストラクター
  • setUp()中にあるActivityInstrumentationTestCase2#getActivity()
  • 同様にsetUp()中にあるActivityInstrumentationTestCase2#getInstrumentation()
ですね。

コンストラクターではテスト対象のアクティビティ・クラスをActivityInstrumentationTestCase2に渡してやります。「総称型(ジェネリクス)でクラスを指定しているんだから、なんでわざわざクラスを通知してやる必要があるの?」という声も聞こえそうですね。すこしだけ解説すると、タイプパラメーター<T>というのは、ただただタイプとして機能するだけで、java.lang.Class<?>オブジェクトのような機能や、役割を一切持ちません。

したがって、

        Class clz = T.class;

とか、

        boolean isInstance = object instanceof T;

とか、

        T object = T.newInstance();

などはすべてコンパイルエラーになります。

これよりも詳しい説明は私が説明するよりも他にもっと優れた解説があります。Java総称型メモなどを参考にしてください。

話を元に戻すと、ActivityInstrumentationTestCase2で、テスト対象のアクティビティを起動するために、そのクラスの情報が必要なわけですね。さて、これジェネリクスで指定したクラスと異なるクラスを渡したらどうなるのか?わかりませんねぇ。あとでやってみましょう。まぁ、予想できる結果として、この後の解説で述べる#getActivity()java.lang.ClassCastExceptionが発生するように思います。

setUp()中にあるActivityInstrumentationTestCase2#getActivity()はActivityを起動して、参照を返すメソッドです。これにより、テストコードはテスト対象のアクティビティをそのコンテクスト上で起動することが可能になります。テストはそれ自体でひとつのアプリケーションですが、テストアプリケーションとは別のスレッド上にテスト対象のアプリケーションが起動するのです。これをsetUp()に置くことで、毎回のテストがアクティビティを起動した状態で実行できます。

最後に、setUp()中にあるActivityInstrumentationTestCase2#getInstrumentation()です。このInstrumentationは、アクティビティの起動とは何の関係もありませんが、アクティビティのUIスレッドと同期をとるような場合や、UIに対して操作を行う場合に必要になるオブジェクトです。今回のテストアプリケーションでも使いますので、最初にその参照を取得しておきます。

では、テストコードをどうぞ。


    public void testPressButton() {
        // EditText にフォーカスを当てる ---- (1)
        EditText editText = (EditText)activity.findViewById(
                orz.mikeneck.hello.R.id.edit_text);
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                editText.requestFocus();
            }
        });
        // UIとの同期をはかる ---- (2)
        instrumentation.waitForIdleSync();
        // UIにキーを送る ---- (3)
        sendKeys(KEYCODE_SHIFT_LEFT);
        sendKeys(KEYCODE_A);
        sendKeys(KEYCODE_N);
        sendKeys(KEYCODE_D);
        sendKeys(KEYCODE_R);
        sendKeys(KEYCODE_O);
        sendKeys(KEYCODE_I);
        sendKeys(KEYCODE_D);
        // 前提条件の確認 ---- (4)
        assertEquals("Android", editText.getText().toString());
        // Button をクリックする ---- (5)
        Button button = (Button)activity.findViewById(
                orz.mikeneck.hello.R.id.button);
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                button.performClick();
            }
        });
        // UIとの同期をはかる ---- (6)
        mInstrumentaion.waitForIdleSync();
        // TextView の値をテストする ---- (7)
        TextView textView = (TextView)findViewById(
                orz.mikeneck.hello.R.id.edit_text);
        assertEquals("Hello,Android", textView.getText());
    }


大雑把な解説ですが、
  • EditTextにフォーカスを当てます。これはUI上の操作に当たるので、Activity#runOnUiThread(java.lang.Runnable)を使用します。
  • UIとの同期をはかります。別スレッド(アクティビティ)との同期はInstrumentationwaitForIdleSync()を用います。
  • UIにキーを送ります。android.view.KeyEventをstatic importしておいてください。
  • EditTextにキーを送ったので、それが反映されていることをテストします。想定される値は「Android」です。
  • Buttonをクリックします。これはUI上の操作に当たるので、Activity#runOnUiThread(java.lang.Runnable)を使用します。
  • UIとの同期をはかります。別スレッド(アクティビティ)との同期はInstrumentationwaitForIdleSync()を用います。
  • Buttonをクリックしたので、仕様通りにTextViewのテキストが変更されているかテストします。想定される値は「Hello,Android」です。
となります。

ポイントは
  • UI上の操作は必ずActivity#runOnUiThread(java.lang.Runnable)を使用する。
  • UI操作後はInstrumentationwaitForIdleSync()を用いる。
です。

#runOnUiThread(java.lang.Runnable)を使わないとUIが操作できないのは、他のアプリケーションからいくらでも値を変更するなどの改変が行われてしまうからというセキュリティ的な側面があるのかと思われます。(Javaそんなに詳しいわけではないので、そのあたりを突っ込んでくれる人、大募集)

二番目のInstrumentationwaitForIdleSync()ActivityInstrumentationTestCase2の真骨頂です。UIスレッドとの同期をはかりスムーズにテストを実行出来るようなオブジェクト類が提供されており、それがActivityInstrumentationTestCase2です。別スレッドのUIと同期するこの能力はテストの実行においてかなり強力な機能です。



こんな感じで、非常に簡単なテストでしたが、みなさまもUI上で不具合を発見したら、なるべくテストコードを書いて、それらを再現できるようにしてみてください。きっと、素晴らしいUIをもつアプリケーションが作成できると思います。

0 件のコメント:

コメントを投稿