Robolectric-InstrumentationでテストをRobolectricとAndroid両方で走らせる

Table of contents:


概要

テストクラスをAndroid Instrumentation TestsとRobolectric Tests両方で走らせる - Islands in the byte stream のハックをライブラリ化しました。

gfx/Robolectric-Instrumentation - Java - GitHub

Ormaのほうはこれに書き換え済みです。またすでにRoblectricを導入していたAndroidアプリをRobolectric-Instrumentationに移行する実例は以下から。

さて、これはRobolectricでAndroid InstrumentationのAPIのサブセットを実装することにより、同じテストコードを両方のテストフレームワークで走らせられるようにするものです。なおEspressoを使うテストは移植できません*1

新規での導入手順は以下のとおりです。 dependenciesを除けば example project が完成形です。

Robolectric Instrumentationを導入する

dependenciesに書くだけです。他にも必要なライブラリがあれば書いてください。

dependencies {
    testCompile 'com.github.gfx.android.robolectricinstrumentation:robolectric-instrumentation:3.0.2'
    testCompile 'junit:junit:4.12'
}

testInstrumentationRunnerを設定する

これは androidTest でJUnit4を走らせるために必要な設定です。Robolectric-InstrumentationはJUnit4にしか対応していません。

android {
  defaultConfig {
    testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
  }
}

robolectric.propertiesを導入する

サブプロジェクトで app があるとすれば、 app/src/test/resources/roblectric.properties に設定ファイルを置きます。これはRobolectric全体の設定に使うファイルで、Robolectric-Instrumentationを使う場合はテストクラスではRobolectric APIを使えないのでこの設定ファイルを使う必要があります。またRobolectric-Instrumentation でも project という設定が追加で必要です。

最低限必要な設定は以下のとおりです。

# サブプロジェクトの名前
project=app
# RobolectricでシミュレートするAPI level
sdk=16

strings.xml などのリソースにアクセスしたいときは更に BuildConfig の設定をする必要があります。

constants=com.example.app.BuildConfig

src/testsrc/androidTest にシンボリックリンクする

(cd app/src && ln -s test androidTest)

テストをAndroid Instrumentation Testing API準拠にする

Robolectric-Instrumentationの実体は com.android.support.test:runner の互換実装です。具体的には、 AndroidJUnit4InstrumentationRegistry です。Robolectric APIではなくこれらを使うように書き換えます。

+ @RunWith(AndroidJUnit4.class)
- @RunWith(RobolectricGradleTestRunner.class)
- @Config(constants = BuildConfig.class, sdk = 16)
+ Context context = InstrumentationRegistry.getTargetContext();
- Context context = RuntimeEnvironment.application;

これで、 ./gradlew test./gradlew androidTest 両方で動くようになるはずです。

ただEspressoなどviewを含めたテストはできませんし、Handlerまわりを使っていると動かないなどのRobolectric特有の現象を考慮する必要があります。Ormaでは特に問題なく移植できましたが、これはほとんどSQLite関連のAPIしか使っていないからです。

FAQ

Unit Tests を走らせると Stub! となって落ちる / Android Instrumentation Tests を走らせると no tests になる

Android Studio 1.5の時点では、同じプロジェクト(モジュール)で Unit TestsAndroid Instrumentation Tests を切り替えるとAndroid Studioがテスト設定をうまく認識できなくなるようです。Edit Configurationsで起動設定を一度消して作りなおすとうまく行きますが、面倒なのでただ走らせるだけならコマンドラインから実行するのが無難です。

Espressoと併用したい / Android Instrumentation Testsのときだけ走るテストを作りたい

実行環境を判定するヘルパメソッドを作って assumeThat() で条件付きスキップするといいと思います。

Robolectric-Instrumentation/.../ActivityTest.java

package com.github.gfx.android.robolectricinstrumentation;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import android.app.Activity;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;

@RunWith(AndroidJUnit4.class)
public class ActivityTest {

    boolean runOnAndroid() {
        return System.getProperty("java.vm.name").equals("Dalvik");
    }

    boolean runOnRobolectric() {
        return !runOnAndroid();
    }

    @Rule
    public ActivityTestRule<MainActivity> mainActivityRule
       = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void testPerformClick() throws Exception {
        assumeTrue("Robolectric does not support Espresso", runOnAndroid());

        onView(withId(R.id.button))
                .perform(click());
    }
}

*1:InstrumentationRegistry経由でActivityを起動することくらいならできますが