はじめに
Realmはめっちゃ速いDBですね。めっちゃ速いのは良いのですが、Android版のRealmは、ネイティブに依存をしているのでそのままではJVMでテストを行うことができません。
ローカル環境ならばGenymotionを利用することでエミュレーターの起動速度や実効速度をカバーすることができますが、CI環境になると話は変わってきます。
エミュレーターの起動時間は遅いし、テストの実行にも時間がかかってしまいます。カジュアルにフルテストを実行できる環境じゃないとCIは長続きしませんね。
そこで、今回はモックを利用することでRealmをJVM上でテストをする方法を解説します。
面倒臭人向け
realm/realm-javaのunitTestExampleの中に今回やってるのと同じ方法でテストを行うコードがあります(記事を書き始めてから気がついた...
依存関係
モックの実現にはpowermockを使います。 Realm は final class
として宣言されているのですが、 powermock
であれば final class
でもモックのインスタンスを作ることができてヤバいですね。
また、テストランナーにはRobolectricを利用します。JVMでAndroidのAPIを実行してくれるので、Activityなどのコンポーネントもテストができるようになります。
build.gradle
はこんな感じになります。
def powerMockVersion = '1.6.3'
dependencies {
compile 'io.realm:realm-android:0.86.0'
testCompile "org.robolectric:robolectric:3.0"
testCompile "org.mockito:mockito-core:1.10.19"
testCompile "org.powermock:powermock-module-junit4:${powerMockVersion}"
testCompile "org.powermock:powermock-module-junit4-rule:${powerMockVersion}"
testCompile("org.powermock:powermock-api-mockito:${powerMockVersion}") {
// to exclude wrong version of hamcrest-core. use mockito-core instead.
exclude module: 'mockito-all'
}
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile "org.powermock:powermock-classloading-xstream:${powerMockVersion}"
}
ソースコードの場所
ソースコードを書く場所なんかは、 src/test/java
以下に書きます。Androidのユニットテストを高速化するに以前書いたので、参考まで。
プロジェクト設定とか
Robolectric
を使うときは、リソースの解決のために自前のテストランナーを用意してあげるのが良いっぽいです。app/src/test/java
以下の適当なパッケージに以下のクラスを用意します。
package net.numa08.test;
import net.numa08.gistlist.BuildConfig;
import org.junit.runners.model.InitializationError;
import org.robolectric.RobolectricTestRunner;
public class CustomRobolectricTestRunner extends RobolectricTestRunner {
public CustomRobolectricTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
String buildVariant = (BuildConfig.FLAVOR.isEmpty() ? "" : BuildConfig.FLAVOR + "/") + BuildConfig.BUILD_TYPE;
System.setProperty("android.package", BuildConfig.APPLICATION_ID);
System.setProperty("android.manifest", "build/intermediates/manifests/full/" + buildVariant + "/AndroidManifest.xml");
System.setProperty("android.resources", "build/intermediates/res/merged/" + buildVariant);
System.setProperty("android.assets", "build/intermediates/assets/" + buildVariant);
}
}
また、Realmのモックを作る上で変なところでNullPointerException
が出ないよう気を使ったり、またモック用のRealmResult
インスタンスを作るためのユーティリティークラスを作っておくと楽です。
ただし、これらのメソッドはパッケージプライベートのメソッドなのでアクセスをするため、io.realm
パッケージに以下のクラスを作ります。
package io.realm;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.util.List;
import static org.mockito.Matchers.anyInt;
import static org.powermock.api.mockito.PowerMockito.mock;
import static org.powermock.api.mockito.PowerMockito.when;
public class MockRealm {
public static Realm mockRealm() {
final Realm realm = mock(Realm.class);
realm.handlerController = new HandlerController(realm);
return realm;
}
public static <T extends RealmObject> RealmResults mockRealmResult(Realm realm, final List<T> data) {
final RealmResults results = mock(RealmResults.class);
when(results.get(anyInt())).then(new Answer<T>() {
@Override
public T answer(InvocationOnMock invocation) throws Throwable {
final int arg = (int)invocation.getArguments()[0];
return data.get(arg);
}
});
results.realm = realm;
return results;
}
}
とりあえず準備完了ですね。
アプリケーションの実装
今回は細かいところは無視しますが、ListFragment
にRealmに保存してあるデータ一覧の表示をするアプリを作ります。
public class GistListFragment extends ListFragment {
// テスト用に Realm は外部から設定可能にしています
Realm realm;
@Override
public void onDestroy() {
if (realm != null) {
realm.close();
}
super.onDestroy();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Realmのクエリを実行します。データの保存なんかは別のところでやってあることを、
// 想定しています。
final RealmResults<Gist> gists = getRealm().allObjects(Gist.class);
final BaseRealmAdapter adapter = new GistListAdapter(getContext(), gists);
setListAdapter(adapter);
}
// Realm の取得は private な Getter を利用することで、
// テスト時に振るまいを変更できます。
private Realm getRealm() {
if (realm == null) {
realm = Realm.getInstance(getContext());
}
return realm;
}
}
テストを書く
@RunWith(CustomRobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP, manifest = "src/main/AndroidManifest.xml")
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*"})
@PrepareForTest({Realm.class, RealmResults.class})
public class GistListFragmentTest {
@Rule
public PowerMockRule rule = new PowerMockRule();
Realm testRealm;
GistListFragment fragment;
List<Gist> gists;
@Before
public void initRealm() throws IOException, JSONException, ParseException {
testRealm = MockRealm.mockRealm();
// test/resources 以下にファイルを置いておくと、テストデータとして利用できて便利です
final InputStream is = GistListFragmentTest.class.getClassLoader().getResourceAsStream("public_gist.json");
final String json = IO.load(is);
final JSONArray jsonArray = new JSONArray(json);
gists = new ArrayList<>();
for (int i = 0; i < jsonArray.length(); i++) {
final JSONObject jsonObject = jsonArray.getJSONObject(i);
final Gist gist = new Gist(jsonObject);
gists.add(gist);
}
final RealmResults results = MockRealm.mockRealmResult(testRealm, gists);
// パラメータに関係なく、テスト用の RealmResults を返します
when(testRealm.allObjects((Class<RealmObject>) any())).thenReturn(results);
fragment = new GistListFragment();
fragment.realm = testRealm;
}
@Test
public void loadGists(){
// Fragment をアタッチするだけで実装が無い TestActivity を用意しておくと、
// Fragment のライフサイクルをテストの中で実行できて便利です
final ActivityController<TestActivity> activityController= Robolectric.buildActivity(TestActivity.class).create().start();
activityController.get().getSupportFragmentManager()
.beginTransaction()
.add(fragment, "gist_list")
.commit();
activityController.resume();
assertThat(fragment.getListView(), is(not(nullValue())));
assertThat(fragment.getListAdapter(), is(not(nullValue())));
final GistListAdapter adapter = (GistListAdapter) fragment.getListAdapter();
// とりあえず、用意したアダプターがViewを返すことをテストします
for (int i = 0; i < gists.size(); i++) {
final GistView view = (GistView) adapter.getView(i, null, fragment.getListView());
assertThat(view, is(not(nullValue())));
final Gist g = view.getGist();
assertThat(g.getId(), is(gists.get(i).getId()));
}
}
}
まとめ
Realmを利用したアプリケーションを、JVMでテストする方法について書きました。なお、ここで紹介したサンプルコードを利用したプロジェクトを用意しておきました。アプリの全体を見てみたい方は是非参考にしてください。
テストを実行する上で、ライブラリの実装や振る舞いに依存をするよりも自分でモックを利用できる方が大体の場合都合が良いし、テストの目的にも適っている場合が多いのではないでしょうか。
PowerMock のお陰で色々と好き勝手にできるので便利ですね。