はじめてRobolectric(robolectric:3.0)を使用してテストを書いた時の備忘録。
Robolectric公式ページ
API levelなんちゃらと言われてテストが失敗する。
現象
Robolectric公式ページ-Getting Started
のコードを参考にテストを実行したら、Robolectric does not support API level... のエラーが発生。
java.lang.UnsupportedOperationException: Robolectric does not support API level 22.
...
対応
RobolectricはAPI level 21までしか対応していない(2016/2/13現在)。
以下のようなエラーが出るようなら、ConfigアノテーションでAPI levelを明示的に指定する必要がある。
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
Fragmentが指定したLayoutに表示されてることのテストがうまくいかない
現象
Activityを起動した際に、Fragmentが指定したLayout上に表示されていることをテストしたいが、テスト結果がnullになってしまう。
@Test
public void 起動時にFooFragmentがLayoutに表示されること() {
Fragment fragment = testActivity.getSupportFragmentManager().findFragmentById(R.id.fragment_container);
assertThat(fragment, not(nullValue()));
}
以下のコードだとぬるぽが発生
@Test
public void 起動時にFooFragmentがLayoutに表示されること() {
Fragment fragment = testActivity.FooFragmentManager().findFragmentByTag(FavSiteFragment.TAG);
// Fragmentが設定されているLayoutのIdを取得
int actual = ((ViewGroup) fragment.getView().getParent()).getId(); // ここでぬるぽが起きる
int expected = R.id.fragment_container;
assertThat(actual, is(expected));
}
対応
テスト用のActivityを構築・取得する箇所で、Robolectric.buildActivity(MainActivity.class).create().get()と記述してしまっていたため、onCreateまで実行された状態でテストしていた。
つまり、FragmentがLayoutに表示される前の状態でテストを実行してしまっていた。
Activityの画面が構築されるタイミングである、onStartまで実行してやる必要がある。
Acrivityのライフサイクル
private MainActivity testActivity;
@Before
public void setUp() {
// onStartまで実行されたAcrivityオブジェクト
testActivity = Robolectric.buildActivity(MainActivity.class).create().start().get();
}
FragmentTestUtil.startFragmentを使用するとエラーが発生する
現象
Fragmentのテストを行うために、FragmentTestUtil.startFragment(Fragment fragment)を使用すると例外が発生する。
FragmentTestUtilドキュメント
対応
FragmentTestUtil.startFragment(Fragment fragment)を使用すると、FragmentTestUtil.$FragmentUtilActivity上でFragmentを動かそうとする。FragmentTestUtil.startFragment(Fragment fragment, Class<? extends FragmentActivity> fragmentActivityClass)を使用して、Activityを指定してやればよい。
GitHub-FragmentTestUtilソース
@Test
public void test() {
FooFragment fragment = new FooFragment();
FragmentTestUtil.startFragment(fragment, MainActivity.class);
...
JMockitoと組み合わせて使用すると、例外が発生する (訂正あり)
2016/02/05追記
下記の方法で一応は動作したが、謎のエラーが頻発したため、RobolectricのCustom Shadow使用する方法に切りかえました。
Custom Shadowの使用方法については、公式ドキュメント通り書いたのにCustom Shadowクラスが使用できないを参照してください。
Mockitと組み合わせて使用した時は普通に動作したし、PowerMockとかも動くとの情報を見かけたので、JMockito以外のモックライブラリを使用することも検討すべきだと思う。
JMockitoの更新履歴をみると、
JMockit now works fine with the Robolectric Android testing tool (tested with Robolectric 2.2 and 2.3).
とあるので、Robolectric3.xから問題が再発したということなのかな?
http://jmockit.org/changes.html
現象
JMockitと組み合わせて、一部クラスをモックに置きかえた以下のようなテストを実行するとIllegalStateExceptionが発生する。
RobolectricGradleTestRunnerをRunWithアノテーションで指定せずにテストを実行すると正常にテストが実行される(JMockitも想定通り動作する)。
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class SiteFragmentTest {
@Mocked
Foo foo;
@Test
public void test() {
new NonStrictExpectations() {
{
foo.getName();
result = "foo";
}
...
java.lang.IllegalStateException: Invalid place to record expectations
...
対応
JMockitのバージョンを変えてみたり(JMockit-Development history)、RobolectricGradleTestRunnerを継承したテストランナーを作成したり(StackOverflow-can jmockit and robolectric coexist?)したがどれもうまくいかず。
Robolectricと併用したケースではないが、MockUpを使用すれば動くとの情報があったので、試してみると一応動作した。
しかし、もっと良い方法はないものか……
今更SaStrutsのユニットテストとモックの残念な関係に泣く
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class SiteFragmentTest {
@Test
public void test() {
new MockUp<Foo>() {
@Mock
public String getName() {
return "foo";
}
};
...
公式ドキュメント通り書いたのにCustom Shadowクラスが使用できない
Robolectricではモックライブラリを使用する代わりにCustom Shadowクラスを作成することで、特定のクラスの振る舞いを置きかえてテストを実行することができる。
現象
公式ドキュメント通り、Custom Shadowクラスを作成し、テストクラスのConfigアノテーションにShadowクラスを指定したのに振る舞いが置きかわらない。
// 公式ドキュメントより
@Implements(Foo.class)
public class FooShadow {
// barメソッドの振る舞いを置きかえ
@Implementation
public void bar() {
// do nothing
}
}
}
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, shadows={FooShadow.class})
public class BazTest {
// ...
対応
CustomShadowクラスを使用するためには、RobolectricがShadowクラスに置きかえるクラスを知っている必要がある。
そのため、RobolectricGradleTestRunnerのかわりに、これを継承したカスタムテストランナー作成し、その中でShadowクラスに置きかえるクラスを登録してやればよい。
public class CustomTestRunner extends RobolectricGradleTestRunner {
public CustomTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
@Override
public InstrumentationConfiguration createClassLoaderConfig() {
// Shadowクラスに置きかえるクラスを登録addInstrumentedClassで登録
return InstrumentationConfiguration.newBuilder()
.addInstrumentedClass(Foo.class.getName())
.build();
}
}
// 作成したカスタムテストランナーを使用
@RunWith(CustomTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, shadows={FooShadow.class})
public class BazTest {
// テスト実行時、Fooクラスを使用している箇所がFooShadowクラスに置きかえられる
// ...
なお、作成したCustom ShadowクラスはConfigアノテーションに指定してテストを実行した時のみ置きかわるようになっている。Configアノテーションに指定していない場合は元のクラス(上記の場合はFooクラス)がそのまま使用される。