2013/03/05

Eclipse の Android JUnit Test は 2回実行される

Android で JUnit Test をしていた時、ちょっと気になる現象を発見してしまいました。
たまたま、android.app.Application を継承した独自実装の Application を使用して、その onCreate() にログを仕込んでいた状態だったので気づいたのです。
以下、その詳細です。

コード

実際のコードをあげるのは大変なので、以下に簡略化したものをあげておきます。

MyApplication.java (Target)
public class MyApplication extends Application {
    private static final String TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();

        Log.d(TAG, "onCreate");
    }
}

AndroidManifest.xml (Target)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.kokufu.android.test.sampletarget"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        
        <!-- 省略 -->
        
    </application>

</manifest>


このアプリに対し、以下のようなテストを作成。
(テストとしての役割は全く果たしていませんが…)

MyTest.java (Test)
public class MyTest extends TestCase {
    private static final String TAG = "MyTest";

    public void testA() {
        Log.d(TAG, "testA");
    }

    public void testB() {
        Log.d(TAG, "testB");
    }
}

Application.onCreate() が2度呼ばれている

Eclipse から
Run → Run As → Android JUnit Test
と実行してみると、
以下の様な結果が。
onCreate が2度呼ばれています。
間違えて 2度実行してしまったわけではありません。
たった1度、実行操作を行っただけです。

さらによく見てみると、最初の onCreate だけ PID が異なります。
どうも、別プロセスで 2回実行されているようですが、テスト自体は2回目だけ実行されるようです。

仕様です

何か私のテストの書き方に問題があるのだろか…
悩みながらも、検索してみると以下のスレッドにたどり着きました。

Unit tests run twice from Eclipse? - Google グループ

This is expected, and will occur for any test execution. The Android Eclipse plugin will issue two commands to the device when running tests - the first is a command to just list all the tests that will be executed. This is needed to populate the JUnit pane. The second command is the one that actually runs the tests.

(意訳)
それは想定通りの動作で、どんなテストでも起こります。The Android Eclipse plugin は テストの実行時に2つのコマンドを実行します。一つは全テストのリストを取得するだけのコマンド。これは、JUnit ペインに テストのリストを表示するために使用されます。もう一つは実際にテストを実行するためのコマンドです。


うん。つまり、「仕様」ってことね。
確かに、JUnit ペインには実行前からテストのリストが表示されて、テストの結果が出るたびに更新されていくなぁ。

ちなみに、JUnit ペインというのは、JUnit テストを実行した時に自動的に表示される以下の様な View のことです。
JUnit pane

1度目かどうかを見分ける方法

1度目の実行が終了すると、アプリケーションはプロセスごと殺されてしまいます。
つまり、2度目の実行はまっさらな状態からのスタートになるので、2度実行されることを特に気にする必要はありません。
ただ、せっかくなので、どのように1度目と2度目を切り分けているのか調べてみました。

android.test.InstrumentationTestRunner のコードを読んでみると、AndroidTestRunner.setSkipExecution を呼んでいる箇所を見つけました。
以下、その抜粋です

InstrumentationTestRunner.java (抜粋)
package android.test;

public class InstrumentationTestRunner extends Instrumentation implements TestSuiteProvider {
    private static final String ARGUMENT_LOG_ONLY = "log";

    private AndroidTestRunner mTestRunner;

    @Override
    public void onCreate(Bundle arguments) {
        super.onCreate(arguments);
        mArguments = arguments;

        boolean logOnly = false;

        if (arguments != null) {
            logOnly = getBooleanArgument(arguments, ARGUMENT_LOG_ONLY);
        }

        mTestRunner = getAndroidTestRunner();
        mTestRunner.setSkipExecution(logOnly);

        start();
    }

    private boolean getBooleanArgument(Bundle arguments, String tag) {
        String tagString = arguments.getString(tag);
        return tagString != null && Boolean.parseBoolean(tagString);
    }
}

どうも、log というキーで Bundle につまれているようです。

というわけで、以下の様に独自の InstrumentationTestRunner を作成し、onCreateを Override してやることで 1回目なのか、2回目なのかを区別することが出来そうです。

MyTestRunner.java (Test)
public class MyTestRunner extends InstrumentationTestRunner {
    private static final String TAG = "MyTestRunner";

    @Override
    public void onCreate(Bundle arguments) {
        super.onCreate(arguments);

        boolean logOnly = getBooleanArgument(arguments, "log");
        Log.d(TAG, "logOnly " + logOnly);
    }

    private boolean getBooleanArgument(Bundle arguments, String tag) {
        String tagString = arguments.getString(tag);
        return tagString != null && Boolean.parseBoolean(tagString);
    }
}

AndroidManifest.xml (Test)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.kokufu.android.test.sampletarget.test"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" />

    <instrumentation
        android:name=".MyTestRunner"
        android:targetPackage="com.kokufu.android.test.sampletarget" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <uses-library android:name="android.test.runner" />
    </application>

</manifest>

以下、結果です。

一度目は logOnly が true、2度目は false になっているのがわかります。

1 件のコメント:

師子乃 さんのコメント...

こんにちは。

仕様が分かると面白いですね。