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での書き換えで十分対応できると思います。