Hatena::ブログ(Diary)

_development, RSSフィード Twitter

2012-09-10

JUnit4 works on Android

AndroidでJUnit4を使うためのライブラリAndroidJUnit4 を公開しました。

Androidは標準状態ではJUnit4が使えませんが、上記ライブラリを使えば可能になります。

このエントリでは、AndroidでJUnit4が使えない理由と、それを可能にするためライブラリで行なっていることの概要を述べます。


AndroidでJUnit4が使えない理由

AndroidでJUnit4が動かないのは何故なのでしょうか?

もちろん、Androidに同梱されているJUnitは3.x系なので、そのままではJUnit4は動作しません。

しかし、JUnit4にはJUnit4TestAdapterというクラスがあり、これを使えばJUnit3の実行系でJUnit4のテストを実行できます。


JUnit4TestAdapterの利用

package com.uphyca.testing.test;

import static org.junit.Assert.assertTrue;
import junit.framework.JUnit4TestAdapter;

import org.junit.Test;

public class HelloJUnit4 {

    public static junit.framework.Test suite() {
        return new JUnit4TestAdapter(HelloJUnit4.class);
    }

    @Test
    public void helloJUnit4() {
        assertTrue(true);
    }
}

しかし実行するとエラーになります。


android.test.suitebuilder.TestSuiteBuilder$FailedToCreateTests
    testSuiteConstructionFaild

java.lang.RuntimeException: Exception during suite construction
at android.test.suitebuilder.TestSuiteBuilder$FailedToCreateTests.testSuiteConstructionFailed(TestSuiteBuilder.java:238)
at java.lang.reflect.Method.invokeNative(Native Method)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:169)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.java:154)
at android.test.InstrumentationTestRunner.onStart(InstrumentationTestRunner.java:545)

getAllTests()のオーバーライド

別の方法、InstrumentationTestRunnerの拡張ポイントであるgetAllTests()をオーバーライドして実行してみます。


    <instrumentation
        android:name="com.uphyca.testing.test.MyInstrumentationTestRunner"
        android:targetPackage="com.uphyca.testing.test" />

package com.uphyca.testing.test;

import junit.framework.JUnit4TestAdapter;
import junit.framework.TestSuite;
import android.test.InstrumentationTestRunner;

public class MyInstrumentationTestRunner extends InstrumentationTestRunner {

    @Override
    public TestSuite getAllTests() {
        TestSuite ts = new TestSuite();
        ts.addTest(new JUnit4TestAdapter(HelloJUnit4.class));
        return ts;
    }
}

これもエラーになります。

テストは実行されずコンソールに次のようなエラーメッセージが表示されます。


Test run failed: Instrumentation run failed due to 'java.lang.ClassCastException'

これらのエラーの原因はInstrumentationTestRunnerがJUnitを扱う方法に原因があります。

InstrumentationTestRunnerは、全てのテストをTestCaseクラスにパースして単一のリストに展開します。

例えば次のようなテストがあるとします。

このクラスはテストメソッド単位に "testFoo"、"testBar"というnameで識別される2つのMyTestインスタンスにパースされ、InstrumentationTestRunnerの内部的なリストに格納されます。


package com.uphyca.testing.test;

import junit.framework.TestCase;

public class MyTest extends TestCase {

    public void testFoo() {
    }

    public void testBar() {
    }
}

まとめ

この挙動のため、実行されるテストは全て、TestCaseのコレクションであるTestSuiteクラス、またはTestCaseクラスである必要があります。

先のJUnit4TestAdapterは、いずれのクラスでもなくTestインターフェイス(とその他のJUnit4用のインターフェイス)を実装しているだけです。

このため、ある時点でTestSuiteまたはTestCaseにキャストされようとしてクラッシュします。

本来JUnit3は、Testインターフェイスを基に構築されておりJUnit4TestAdapterもこの思想に基づいたデコレーターとして実装されています。

しかし、InstrumentationTestRunnerはこれに基づいていないために先述の結果になります。

これを踏まえると、JUnit4TestAdapter的なものをTestSuite、TestCaseとして実装すれば実行は可能です。

しかし、この場合もやはり実行前にテストメソッド単位にパースされてしますので、@BeforeClassや、TestSetupクラスによるテストクラスレベルのフィクスチャーが毎回実行されてしまったり、テストケースの数を正しく報告できなかったりします。

以上がAndroidでJUnit4が実行できない理由です。


AndroidでJUnit4を使うために

AndroidでJUnit4を使うには、先述のテストメソッド単位へのパースを避けてJUnitの思想に基づいた振る舞いを実装すれば良いことになります。


InstrumentationTestRunnerを継承したランナーを作成する

これにはInstrumentationTestRunnerを継承し、問題のある箇所をオーバーライドすることで可能になります。

テストの実行自体はInstrumentationTestRunnerではなく、その基底クラスであるInstrumentationを継承することでも可能です。また、アプリケーションにしてしまう手もあります。

しかし、Eclipse/ADT(の現在のバージョン)から実行する場合は、InstrumentationTestRunnerを継承する必要があります。

InstrumentationTestRunnerは内部的にTestSuiteBuilder, AndroidTestRunner, TestGroupingといったクラスを使ってテストのパースと実行を行なっています。

これらを、よかれにオーバーライド、またはリプレースします。

そのようにして作成したのがJUnit4InstrumentationTestRunnerです。

これ、またはそのサブクラスをテストプロジェクトのAndroidManifest.xmlのinstrumentationに指定します。

JUnit4InstrumentationTestRunnerが依存するクラスで変更等したものは com.uphyca.testing.androidパッケージ以下にあります。このパッケージ以下は概ねandroid.testパッケージの元の構成に準じています。


Androidコンテキストのインジェクション

Androidのテストにはコンテキストとして、Context、Instrumentationクラスなどがインジェクトされます。

これらはInstrumentationTestRunnerから取得できますので、それをテストクラスのインスタンスにインジェクトします。これを持ちまわるのは大変なので、ThreadLoacalを使いました。

また、Androidのテストの各種アノテーション @FleakyTest、@UiThreadTestなどは複数回テストメソッドを実行したり、UIスレッドで実行する必要があります。

これらの変更等はcom.uphyca.testing/junitパッケージ以下にあります。このパッケージ以下は概ねorg.junitパッケージの元の構成に準じています。


Androidテストケースの利用

AndroidテストケースをJUnit4で利用するために、テスト用の基底クラスを作成します。

Androidの各テストケースに対応するヘルパークラスを作成し、テスト用の基底クラスからこれに委譲するようにします。ヘルパークラスの初期化と同時にThreadLocalに保持したコンテキストをインジェクトします。

これらはcom.uphyca.testingパッケージ以下にあります。

Androidテストケースと同名の次のクラスを、同じように使います。


  • AndroidTestCase
  • InstrumentationTestCase
  • ActivityInstrumentationTestCase<T extends Activity>
  • ActivityInstrumentationTestCase2<T extends Activity>
  • ActivityTestCase, ActivityUnitTestCase<T extends Activity>
  • ApplicationTestCase<T extends Application>
  • LoaderTestCase
  • ProviderTestCase<T extends ContentProvider>
  • ProviderTestCase2<T extends ContentProvider>
  • ServiceTestCase<T extends Service>
  • SingleLaunchActivityTestCase<T extends Activity>
  • SyncBaseInstrumentation

例えば、android.test.InstrumentationTestCase を使ってやるべきテストには com.uphyca.testing.InstrumentationTestCase を使います。


package com.uphyca.testing.test;

import static org.junit.Assert.assertNotNull;

import org.junit.Test;

import com.uphyca.testing.AndroidJUnit4TestAdapter;
import com.uphyca.testing.InstrumentationTestCase;

public class HelloInstrumentation extends InstrumentationTestCase {

    /**
     * For Eclipse with ADT
     */
    public static junit.framework.Test suite() {
        //Should use AndroidJUnit4TestAdapter for to running AndroidDependent TestCases.
        return new AndroidJUnit4TestAdapter(HelloInstrumentation.class);
    }

    @Test
    public void testPreconditions() {
        assertNotNull(getInstrumentation());
        assertNotNull(getInstrumentation().getContext());
        assertNotNull(getInstrumentation().getTargetContext());
    }
}

JUnit4TestAdapterの拡張

Eclipse/ADTからテストを実行する場合、テストクラスはTestCaseのようなJUnitのクラスであるか、staticなsuite()メソッドを持つ必要があります。

このため、通常のJUnit4のテストのように特定のクラスを継承しない場合はADTから実行できません。

ADTから実行するには、suite()メソッドで、AndroidJUnit4TestAdapterでテストクラスをラップして返します。Androidコンテキストに依存しない通常のテストはJUnitのJUnit4TestAdapterを使います。


package com.uphyca.testing.test;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;

import org.junit.Test;

import com.uphyca.testing.AndroidJUnit4TestAdapter;
import com.uphyca.testing.AndroidTestCase;

public class HelloAndroid extends AndroidTestCase {

    /**
     * For Eclipse with ADT
     */
    public static junit.framework.Test suite() {
        return new AndroidJUnit4TestAdapter(HelloAndroid.class);
    }

    @Test
    public void testPreconditions() {
        assertNotNull(getContext());
    }

    @Test
    public void shouldBeValidPackage() {
        assertThat(getContext().getPackageName(), equalTo("com.uphyca.testing.test"));
    }
}

以上-

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

リンク元
Connection: close