前置き
JavaでJUnit4のテストを書いていたころは、@Before
を付けた(setup()
のような名前の)メソッドの中にテスト対象やその依存関係の初期化処理を書き、テストケースの中で使うもの(テスト対象など)はフィールドに定義してsetup()
メソッドの中でフィールドにセットしていた。
private Foo sut = null; @Before public void setup() { final FooChild child = new FooChild(); sut = new Foo(child); }
個人的には、このテストクラスの文脈として共有したいことをsetup()
に書いて整理するというスタイルが気に入っていたのだが、Kotlinでテストを書くようになってからはそれをやめて、テスト対象やその依存関係をテストクラスのプロパティとして書くようになった。
private val sut: Foo = Foo( FooChild() )
多くの場合はこれで何の問題もないと思うが、気を付けておきたいのはTestRule
を使ってテストのセットアップを行う場合で、特にAndroidアプリの開発においては、テスト対象がLiveData
のプロパティを持っているようなときに使うInstantTaskExecutorRule
や、Kotlin coroutineのテストをするときにTestCoroutineDispatcher
を使うようなときである。
初期化の順序に気を付けよう
ポイントは初期化処理の実行順で、テストではざっくり言って次のようになる。
- (テストクラスの
@BeforeClass
関数) - テストクラスのコンストラクタ(プロパティの初期化処理、
TestRule
のコンストラクタの処理もここで行われる) TestRule.apply()
でStatement.evaluate()
より前に実行される処理(TestWatcher
を実装しているならstarting()
)InstantTaskExecutorRule
ではここでLiveData
内部で使うExecutor
をテスト用のモックに差し替えているTestCoroutineDispatcher
を使ったセットアップではこのタイミングでDispacher
をテスト用のモックに差し替える
- テストクラスの
@Before
関数 - テストケース
先の例ではテスト対象の初期化はリストの2番目で行われるが、テスト対象の初期化の中でLiveData
がアクティブになったり、coroutineのDispacher.Main
などにアクセスするようなことをやっているとその時点でモックしなさい例外*1が出てクラッシュする(androidx.lifecycle.liveData()
など)。なので、テスト対象の初期化はリストの3番目の後に行われるようにしなければならないが、さりとてsetup()
の中で生成してlateinit var
なプロパティにセットするというのも忍びない。手っ取り早く解決するにはlazy
を使うのがいいだろうか。
private val sut: Foo by lazy { val child = FooChild() Foo(child) }
こうしておけばsetup()
やテストケースの中で初めてsut
にアクセスしたときに初期化されるので、モックしなさい例外を回避できる。ただし、lateinit var
の方がすっきり書けるという場合にはそっちの方がいいと思います。テストクラス用のTestRule
を自前で用意してその中でテスト対象を初期化するようなケースでは、lateinit var
の方をバッキングフィールドにして隠すという手も使えるでしょうし。
*1:個人的にそう呼んでいる