Robolectricでテストを書いたときの備忘録

はじめてRobolectric(robolectric:3.0)を使用してテストを書いた時の備忘録。
[Robolectric公式ページ](http://robolectric.org/)

## API levelなんちゃらと言われてテストが失敗する。
### 現象
[Robolectric公式ページ-Getting Started](http://robolectric.org/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を明示的に指定する必要がある。

``` Java
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
```

## Fragmentが指定したLayoutに表示されてることのテストがうまくいかない

### 現象
Activityを起動した際に、Fragmentが指定したLayout上に表示されていることをテストしたいが、テスト結果がnullになってしまう。

``` Java
@Test
public void 起動時にFooFragmentがLayoutに表示されること() {
    Fragment fragment = testActivity.getSupportFragmentManager().findFragmentById(R.id.fragment_container);
    assertThat(fragment, not(nullValue()));
}
```
以下のコードだとぬるぽが発生

``` Java
@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のライフサイクル](http://mixi-inc.github.io/AndroidTraining/fundamentals/2.02.activity-and-fragment.html)

``` Java
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ドキュメント](http://robolectric.org/javadoc/3.0/org/robolectric/util/FragmentTestUtil.html)

### 対応
FragmentTestUtil.startFragment(Fragment fragment)を使用すると、FragmentTestUtil.$FragmentUtilActivity上でFragmentを動かそうとする。FragmentTestUtil.startFragment(Fragment fragment, Class<? extends FragmentActivity> fragmentActivityClass)を使用して、Activityを指定してやればよい。
[GitHub-FragmentTestUtilソース](https://github.com/robolectric/robolectric/blob/master/robolectric/src/main/java/org/robolectric/util/FragmentTestUtil.java)

``` Java
@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も想定通り動作する)。~~

``` Java
@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)](http://jmockit.org/changes.html)、RobolectricGradleTestRunnerを継承したテストランナーを作成したり[(StackOverflow-can jmockit and robolectric coexist?)](http://stackoverflow.com/questions/14780965/can-jmockit-and-robolectric-coexist)したがどれもうまくいかず。~~
~~Robolectricと併用したケースではないが、MockUpを使用すれば動くとの情報があったので、試してみると一応動作した。
しかし、もっと良い方法はないものか……
[今更SaStrutsのユニットテストとモックの残念な関係に泣く](http://www.bunkei-programmer.net/entry/2014/09/07/000946)~~

```Java
@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クラスを指定したのに振る舞いが置きかわらない。

```Java
// 公式ドキュメントより
@Implements(Foo.class)
public class FooShadow {
    // barメソッドの振る舞いを置きかえ
    @Implementation
    public void bar() {
        // do nothing
    }
  }
}
```

```Java
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, shadows={FooShadow.class})
public class BazTest {
// ...
```
http://robolectric.org/custom-shadows/
### 対応
CustomShadowクラスを使用するためには、RobolectricがShadowクラスに置きかえるクラスを知っている必要がある。
そのため、RobolectricGradleTestRunnerのかわりに、これを継承したカスタムテストランナー作成し、その中でShadowクラスに置きかえるクラスを登録してやればよい。

```Java
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();
    }
}
```

```Java
// 作成したカスタムテストランナーを使用
@RunWith(CustomTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21, shadows={FooShadow.class})
public class BazTest {
// テスト実行時、Fooクラスを使用している箇所がFooShadowクラスに置きかえられる
// ...
```
http://stackoverflow.com/questions/31920865/how-to-create-custom-shadows-in-robolectric-3-0

なお、作成したCustom ShadowクラスはConfigアノテーションに指定してテストを実行した時のみ置きかわるようになっている。Configアノテーションに指定していない場合は元のクラス(上記の場合はFooクラス)がそのまま使用される。