Android
unittest
robolectric

Robolectric 3.0基本のキ

More than 1 year has passed since last update.

Android Studio + Robolectricを使った単体テストの基本的な設定やテスト方法の説明です。

Gradle設定

moduleのbuild.gradleに以下を追加

build.gradle
buildscript {
    ...
    dependencies {
        ...
        classpath 'org.robolectric:robolectric-gradle-plugin:1.1.0'
    }
}

apply plugin: 'org.robolectric'

...

dependencies {
    ...
    testCompile 'org.robolectric:robolectric:3.0'
    ...
}

ver2.4ではdependenciesに色々設定しないといけなかったようですが、3.0は1行でうまく動いています。

TestClassの作成

Runnerの指定

まずはテストクラスでRobolectricのRunnerを指定します。

@RunWith(RobolectricTestRunner.class)

後述するShadowクラスを追加するということをしなければ RobolectricGradleTestRunner RobolectricTestRunnerでOKなはず。
Shadowクラスを追加して自前のRunnerを作った場合はその自前Runnerを設定しましょう。

package,buildConfig,sdkの指定

同じくテストクラスでpackage,buildconfig,sdkを指定します。

@Config(packageName = "your.app.package", constants = BuildConfig.class, sdk = 21)

BuildConfig.classは自動生成される自分のアプリのBuildConfigクラスを指定してください。

重要:sdkの設定

sdkはrobolectricがシュミレートするsdkのバージョンです。これを指定しないとAPILevel 16をデフォルトとしてRobolectricが動いてしまいます。アプリ本体のManifestに記載されている各API Levelがあっても、robolectricはManifestを無視してエミュレートするので忘れずに指定したほうが良いです。
また、robolectricはver3.0でもsdk21までしかサポートしていません。今のところ21で不自由ないので何もしていませんが、もし22以降のAPIやクラスを使いたい場合はRobolectricのリリースを待つか、Shadowを自作したほうがいいのではと思います。

@Configで設定できる内容はファイルで外だしできるようですが、自分はテストクラス用の親クラスを作成して設定してしまっています。
ファイル設定の方法はrobolectricのサイトで確認できます。

基本的にはここまででrobolectricの動作準備は整いましたのであとはテストケースを書くだけです。
おそらく以下のようになると思います

@RunWith(RobolectricTestRunner.class)
@Config(packageName = "your.app.package", constants = BuildConfig.class, sdk = 21)
public class SampleTest {

    @Before
    public void setup() throws Exception {
        // do something
    }

    @Test
    public void test_hoge() throws Exception {
        // do test
    }
...

必要なテストケースを書いたら、Junitのテストケースとしてテストを行います。AndroidTestでありません。GradleならtestDebugなどのタスクが該当し、connectedCheckタスクではRobolectricのテストは動作しません。

Shadowクラスの作成

RobolectricではShadowという仕組みでMockクラスを用意することができます。
android.*パッケージのクラスについてはRobolectricがShadowを用意してくれており、自動でShadowが使われる設定になっているためandroid.*だけがエミュレートされてくれれば良い場合はShadowを作成する必要がありません。

それ以外の自前の共通クラスやその他のライブラリのMockクラスが必要な場合はShadowクラスを自作する必要があります。

Shadowクラス作成の流れは以下のとおりです。

  1. Shadowクラスを作成する
  2. Runnerで作成したShadowクラスとShadow対象のクラスを登録する
  3. テストクラスでShadowを使うことを宣言する

ポイントは3の「使うことを宣言する」という点です。これにより、「必要なテストクラスあるいはテストメソッドだけShadowを使い、本当のクラスで動作させたい場合は宣言しなければいいだけ」という取捨選択がしやすくなっています。

では順を追って説明します。

1.Shadowクラスを作成する

Shadowクラスは@ImplementsクラスアノテーションでどのクラスのShadowなのか、@Implementationメソッドアノテーションで、これはShadow独自のメソッドかShadowのメソッドなのかを宣言します。
またShadowクラスのpackageはShadow対象のクラスと一致させる必要はありません。正しく設定すればRunnerが適切にロードしてくれます。これにより、Shadowクラス用のpackageで複数のShadowを管理することができます。

以下はVolleyをShadowsするクラスの抜粋です。

package your.app.package.testtools.robolectric;

...

@Implements(Volley.class)
public class ShadowVolley {

    @Implementation
    public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        // do something if you want!
        return null;
    }

...以下略

2.Runnerで作成したShadowクラスとShadow対象のクラスを登録する

作成したShadowクラスはRobolectricに登録しないとShadowとして動作しません。RobolectricTestRunnerを拡張してShadowMapにShadowクラスを、InstrumentationConfigurationにShadow対象のクラスを登録します。
これはコードを見たほうが理解しやすいと思います。

package your.app.package.testtools.robolectric;
...

public class CustomRobolectricRunner extends RobolectricTestRunner {

    public CustomRobolectricRunner(Class<?> testClass) throws InitializationError {
        super(testClass);
    }

    @Override
    protected ShadowMap createShadowMap() {
        return super.createShadowMap().newBuilder()
                .addShadowClass(ShadowVolley.class)
                .addShadowClass(ShadowFoo.class)
                .addShadowClass(ShadowBar.class)
                .build();
    }

    public InstrumentationConfiguration createClassLoaderConfig() {
        return InstrumentationConfiguration.newBuilder()
                .addInstrumentedClass(Volley.class.getName())
                .addInstrumentedClass(Foo.class.getName())
                .addInstrumentedClass(Bar.class.getName())
                .build();
    }
}

3.テストクラスでShadowを使うことを宣言する

あとは、テストクラスでShadowを使うことを宣言するだけです。
packageやsdkを設定した@Configにshadowsを追加します。

@Config(packageName = "de.lemona.android", constants = BuildConfig.class, sdk = 21,
        shadows = {ShadowFoo.class, ShadowVolley.class})
public abstract class HogeTest {

この例ではShadowBar.classをshadowsに含めていませんので、このテストクラスではBarクラスは本物のBarクラスが動作します。また、@configアノテーションはメソッドにも設定できるので、特定のメソッドだけShadowクラスを使わせる、ということも可能です。

まとめ

以上、基本のキでした。

ここまで書いて改めて思いましたが、RobolectricのShadowはテスト用クラスをテストソース内に実装して、本物のクラスをMockするという古典的な?方法を拡張したものです。(たぶん)
ですが、「Shadowクラスを使う、使わないを実装者側で選択できる」「PackageをShadow対象と一致させる必要がない」という点をアノテーションとTestRunnerでシンプルに実現しており、テストのし易さを向上させつつ実装側へ自由度も提供しています。
個人的にこの2点がとても好印象なポイントです。
(もちろん、今はRobolectricだけではなくMockitoもあわせて2つ方法を適宜使い分けています)

次はShadowの使い方について書こうと思います。