DIの仕組みが導入されていないAndroidプロダクトでのUIテストは、Activity・Fragmentからアクセスされる外部依存の差し替えをどうするかが課題になる場合があります。
テスト実行時にはアクセスしたくない依存がある場合(例:API通信)、これに当てはまります。
ここではコンスタラクタとデフォルト引数を駆使しながら、テストコード上で依存を差し替えできるようにする方法について紹介します。
ActivityとFragmentのコンストラクタインジェクション
ActivityとFragmentは基本的にカスタムのコンストラクタを作成しません。
システムによって引数を持たない空のコンストラクタからインスタンスが生成されるためです。
一方、カスタムコンストラクタの引数にすべてデフォルト値をセットした場合は、引数なしのコンストラクタを追加で生成します。そのコンストラクタからインスタンスを生成したときは、プロパティはデフォルト値で設定されています。
これを利用して、プロダクトコードではデフォルト引数の値を参照し、テストではコンストラクタから実装を差し替えるようにすることで、大きな改修なくUIテストでのコンストラクタインジェクションができるようにします。
テストコードでは、ActivityScenarioやFragmentScenarioといったAPIを使った際に、起動するインスタンスを差し替えできるようにします。
そのインスタンス差し替えを実現するのがInterceptingActivityFactoryとFragmentFactoryです。
InterceptingActivityFactoryを使ったActivityの起動
InterceptingActivityFactoryは、MonitoringInstrumentationというクラスにセットすることができ、このクラスはAndroidのInstrumentation Test上でのみアクセス可能です。
サンプルは次のとおりです。
// 起動したいActivity
// 依存クラスをコンストラクタでうけとる
// デフォルト値にはプロダクトコードで利用する依存を返すようにする
class MyActivity(val dependency: Dependency = DefaultDependency())
// 以下、テストコード
// InstrumentationをMonitoringInstrumentationにキャスト
val monitoringInstrumentation = InstrumentationRegistry.getInstrumentation() as MonitoringInstrumentation
// インスタンス差し替えのセットアップ
monitoringInstrumentation.interceptActivityUsing(object : InterceptingActivityFactory {
// 起動するActivityのインスタンスを返却する関数
override fun create(classLoader: ClassLoader?, className: String?, intent: Intent?): Activity {
// テスト用の依存クラスをセットしたActivityのインスタンス
return MyActivity(TestDependency())
}
// どのクラスの場合にActivity起動の割り込みをするかをbool値で返す
// trueの際に上のcreate関数が呼ばれる
override fun shouldIntercept(classLoader: ClassLoader?, className: String?, intent: Intent?): Boolean {
return className == MyActivity::class.java.name
}
})
// ActivityScenarioでActivityを起動
val intent = Intent(context, MyActivity::class.java)
val scenario = launchActivity<MyActivity>(intent)
scenario.onActivity { activity ->
// ここで取得できるのはInterceptingActivityFactoryで差し替えたActivity
}
補足
- サンプルではInterceptingActivityFactoryの
create
関数内でActivityのインスタンスを作成しています。この生成処理を関数の外から渡すことは可能です。ただ、Activityのインスタンス生成はメインスレッドで実行する必要があり、テストコードはメインスレッドでは実行されません。なので、Activityを返すFunction Typeを渡して、create
関数内でinvoke
するようにすると、汎用的にすることができます - インスタンスの生成が行われたタイミングではActivityのonCreateは実行されていません。一方、依存クラスの中にはActivityからの情報が必要なものもあるかもしれません。コンストラクタで渡すのは実際の依存クラスではなくて依存クラスのFactoryにし、ライフサイクルにあったタイミングで依存クラスのインスタンスの生成を行うほうが望ましいと思います。
- Activityのインスタンス差し替えの仕組みとしてAppcomponentFactoryがありますが、こちらはAPI28以上のみ利用可となっています。
FragmentFactoryを使ったFragmentの起動
FragmentFactoryは、プロダクトコードでも使用可能なAPIで、FragmentManagerにセットすることでFragmentのインスタンス生成をコントロールすることができます。
テストコードではFragmentScenarioの引数に渡すことで利用することができます。(FragmentScenarioの内部で使われるテスト用ActivityのsupportFragmentManagerにセットされます)
サンプルは次のとおりです。
// 起動したいFragment
// 依存クラスをコンストラクタでうけとる
// デフォルト値にはプロダクトコードで利用する依存を返すようにする
class MyFragment(val dependency: Dependency = DefaultDependency())
// 以下、テストコード
// インスタンス差し替えのセットアップ
// FragmentScenarioの引数にセットするだけでOK
val scenario = launchFragmentInContainer<MyFragment>(
factory = object : FragmentFactory() {
// 起動するFragmentのインスタンスを返却する関数
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
if (className == MyFragment::class.java.name) {
return MyFragment()
}
return super.instantiate(classLoader, className)
}
})
scenario.onFragment { activity ->
// ここで取得できるのはFragmentFactoryで差し替えたFragment
}
まとめ
InterceptingActivityFactoryとFragmentFactoryを利用して、UIテストでActivityとFragmentのコンストラクタインジェクションを実現する方法を紹介しました。
DIライブラリは導入していないけど、UIテストを導入したいと思っているAndroidプロダクトの参考になれば幸いです。