読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

robolectric3でShadowクラスを作るメモ

robolectric3のドキュメント通りにShadowクラスを書いてもうまく動かず、結局robolectric自身のソースを読んで理解してめんどくさかったのでメモしておきます。

Shadowクラスを定義する

Shadowクラスは以下の手順で宣言します。

  • クラス宣言に@Implementsアノテーションを付与し、書き換え対象となるクラスを設定する
  • フィールドに@RealObjectアノテーションを付与した書き換え対象のクラスを宣言する。これは本当のオブジェクトの処理を呼び出したい場合に使います。

Shadowクラスはだいたい${application_id}.testtool.shadowパッケージに置いてます。本エントリではGsonBuilderの書き換えを行います。

import com.google.gson.GsonBuilder;

import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.RealObject;

@Implements(GsonBuilder.class)
public class ShadowGsonBuilder {
  @RealObject
  private GsonBuilder realObject;
  //色々
}

メソッドを書き換える

メソッドを書き換えるには@Implementationアノテーションを使います。

@Implementation
public GsonBuilder setDateFormat(String format) {
  return Shadow.directlyOn(realObject, GsonBuilder.class)
          .setDateFormat(format.replace("Z", "X"));
}

ここではGsonBuilderのsetDateFormat(String)を書き換えてます。Gson 2.4以下ではタイムゾーンZが利用できずISO8601のパース時にコケます*1。代わりにXを使わないといけないのですがXはJava7以降でサポートされたものでAndroid上では動作しません*2。という事でsetDateFormat(String)の引数をrobolectricでのテスト時だけ書き換えるようにしてます。

return realObject.setDateFormat(format.replace("Z", "X"))と書くと無限ループしてStackOverflowErrorになります。そこでShadow.directlyOn()を使って本物のクラスのメソッドを呼び出します。

コンストラクタを書き換える

コンストラクタを書き換えたいケースはほぼないと思いますがもし書き換えるなら以下のようになります*3。ポイントは__constructor__というメソッド名とShadow.invokeConstructor()でしょう。

@Implements(Paint.class)
public class ShadowPaint {
  //省略
  @RealObject Paint paint;

  public void __constructor__(int flags) {
    //省略
    Shadow.invokeConstructor(Paint.class, paint, ReflectionHelpers.ClassParameter.from(int.class, flags));
  }

  //省略
}

カスタムRobolectricTestRunnerを作りShadowクラスを登録する

Shadowクラスを作ったら次にカスタムRobolectricTestRunnerを作ってShadowクラスの登録とマッピングを行います。Shadowクラスを追加する度に更新しないといけないので面倒です。

public class MyRobolectricTestRunner extends RobolectricTestRunner {
  public MyRobolectricTestRunner(Class<?> testClass) throws InitializationError {
    super(testClass);
  }
  @Override
  protected ShadowMap createShadowMap() {
    return super.createShadowMap().newBuilder()
            //Shadowクラスを登録する
            .addShadowClass(ShadowGsonBuilder.class)
            .build();
  }
  public InstrumentationConfiguration createClassLoaderConfig() {
    return InstrumentationConfiguration.newBuilder()
            //クラスロード時に書き換え対象となるクラスのFQCNを登録する
            .addInstrumentedClass(GsonBuilder.class.getName())
            .build();
  }
}

テストでカスタムRunnerを使い、Configで利用するShadowを宣言する

Shadowクラスをテストで利用するにはShadowクラスのマッピングを行うRunnerを@RunWithで指定し、さらに@Configアノテーションで利用するShadowクラスを宣言します。

@RunWith(MyRobolectricTestRunner.class)
@Config(shadows = {ShadowGsonBuilder.class})
public class FooTest {
  //...
}

これでこのテスト内ではGsonBuilderが書き換えられます。やったね。

Java標準ライブラリ等は書き換えられないので注意

https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/internal/bytecode/InstrumentationConfiguration.java#L161などに書き換えを除外するパッケージやクラスが定義されてます。書き換わらないぞ!?という時はチェックするとよさそうです。もしかしたらこの辺も設定次第でいけるかもしれません。

とりあえずShadowクラスは最後の手段だと考えておいた方がいいでしょう。ほとんどの場合はrobolectric自身が提供するShadowクラスの利用やpowermockでの書き換えで十分対応できると思います。