Realm アドベントカレンダーが空いてるので参加します。
Android のコポーネントで Realm をどう管理するのか問題
Realmは Closeable
なクラスで、利用をし終わったら close
をしなければなりません。 Realm を利用し終わるとは以下のパターンがあるのでは無いでしょうか。
- Realm への書き込みが終わる
- Realm から読み込んだデータへの参照が終わる
「Realm への書き込みが終わる」は特に意識をする必要は無いでしょう。ちょっと他のDBと比べて特殊なのが2番めの「Realm から読み込んだデータへの参照が終わる」です。
これは言い換えると「RealmQuery の結果を利用する間は、 Realm を close してはならない」と言うことです。そのため、例えば以下のコードを実行するとアプリがクラッシュをします。
// MainActivity.java
public class MainActivity extends Activity {
TextView textView;
@Override
protected void onCreate(Bundle savedInstance) {
// 省略
Realm realm = Realm.getInstance(this);
MyRealmObject realmObject; // MyRealmObject extends RealmObjecrt
realmObject = realm.query(/** your query */).findFirst();
realm.close(); // No!!!
textView.setText(realmObject.getText()); // This Realm instance has already been closed, making it unusable.
}
}
そこで例えば次のように Realm をフィールド変数として利用するパターンが考えられます。
// MainActivity.java
Realm realm;
TextView textView;
@Override
protected void onCreate(Bundle savedInstance) {
realm = Realm.getInstance(this);
realmObject = realm.query(/** your query */).findFirst();
textView.setText(realmObject.getText());
}
@Override
protected void onDestroy() {
realm.close();
super.onDestroy();
}
大体の場合このパターンで問題無いと言えるでしょう。しかし、テストがし難い問題があります。フィールド変数で定義された Realm は確実に onCreate
で初期化されるため、テストの時はテスト用のRealmを参照するとかと言った方法が取りにくくなります。
また、Realmの初期化の方法がコンポーネントに依存してしまうので FirstActivity
と SecondActivity
で利用するRealmが同じである事を保証することができない、また利用するRealmを変更したい場合に変更箇所が多くなってしまいます。まとめると
- Realm に依存する部分のユニットテストのコードが書きにくくなる
- Realm がコンポーネントに依存するため、 同じ Realm の再利用や設定変更がやりにくくなる
と言ったところです。
それ、 AndroidAnnotations ならなんとかなるよ
この問題の解決をするのに、AndroidAnnotationsのDI機能を利用することでわりとなんとかなります。
AndroidAnnotations を利用することで、View などの依存性を Activity, Service, Fragment, BroadcastReceiver, ContentProvider へ注入することができますが、何を隠そうそれ以外のモデルについても依存性注入が可能になります。
この機能を利用して「RealmConfigurator を返すモデル」を定義することで、Realm のコンポーネントへの依存を解消できるようになります。それでは実際にコードを見てみましょう。
ちなみに、テストはRobolectricを利用してJVM上で実行することを想定しています。
// MainRealmConfigurator.java
@Bean
public class MainRealmConfigurator {
private final RealmConfiguration realmConfiguration;
public MainRealmCOnfigurator(Context context) {
realmConfiguration = new RealmConfiguration.Builder(context.getCacheDir())
.setModules(new Module())
.build();
}
public RealmConfiguration getConfiguration() {
return realmConfiguration;
}
}
// RealmFactory.java
// PowerMock を利用するためにラッパーを用意しています。
public class RealmFactory {
public static Realm getInstance(RealmConfiguration config) {
return Realm.getInstance(config);
}
}
// MainActivity.java
@EActivity
public class MainActivity extends Activity {
@EBean
MainRealmConfigurator mainRealmConfig;
Realm realm;
@AfterInject
void initRealm() {
realm = RealmFactory .getInstance(mainRealmConfig.getConfiguration());
}
@Override
protected void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
MyRealmObject realmObject = //省略
textView.setText(realmObject.getText());
}
@Override
protected void onDestroy() {
realm.close();
super.onDestroy();
}
}
@Bean
や @EBean
アノテーションを利用することで Realm の設定をコンポーネントから分離しその結果複数のコンポーネントで同じ設定の Realm を再利用できるようになりました。
ただし、今のところ AndroidAnnotations は「テストの時に別のインスタンスを利用する」機能をサポートしていません。この辺りの機能については、 PowerMock
で補います。
PowerMock を Realm に依存しているプロジェクトで使う方法は、Realmに依存する部分もJVMでテストする話を参考にしてください。
この Activity に対してテストを行うときは
// MainActivityTest.java
@RunWith(CustomRoboRunner.class)
@Config(manifest = Config.NONE, sdk = Build.VERSION_CODES.LOLLIPOP, constants = BuildConfig.class)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"})
@PrepareForTest({Realm.class, RealmFactory.class,MainRealmConfigurator.class})
public class MainActivityTest {
@Rule
public PowerMockRule rule = new PowerMockRule();
RealmConfiguration configuration;
MainRealmConfigurator_ mockConfigurator;
@Before
public void mockRealm() {
final Realm mock = mock(Realm.class);
mockStatic(RealmFactory.class);
when(RealmFactory.getInstance(Matchers.<RealmConfiguration>any())).thenReturn(mock);
}
@Before
public void initConfig() {
configuration = mock(RealmConfiguration.class);
mockConfigurator = mock(MainRealmConfigurator_.class);
when(mockConfigurator.getRealmConfiguration()).thenReturn(configuration);
mockStatic(MainRealmConfigurator_.class);
when(MainRealmConfigurator_.getInstance_(any())).thenReturn(mockConfigurator);
}
@Test
public void injectionTest() {
final ActivityController<MainActivity_> activityController = Robolectric.buildActivity(MainActivity_.class);
activityController.create();
final MainActivity activity = activityController.get();
assertThat(activity.realmConfigurator, is(sameInstance(mockConfigurator)));
assertThat(activity.realmConfigurator.getRealmConfiguration(), is(sameInstance(configuration)));
}
}
今回は RealmConfiguration を確認しているだけですが、本当なら適当にモックをした RealmResult
を用意してUIなんかの変更をテストする物でしょう。
まとめ
AndroidAnnotations を利用して Realm の初期化をコンポーネントに依存せず行う方法を紹介しました。 AndroidAnnotations の機能の限界もあって、一部は PowerMock で補っています。
また、 PowerMock で補っていますが、 Instrumentation Test の方だと PowerMock が利用できないので別の手段を考える必要があります。