fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

テスト対象を初期化する際にはlazyを使う

前置き

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を使うようなときである。

初期化の順序に気を付けよう

ポイントは初期化処理の実行順で、テストではざっくり言って次のようになる。

  1. (テストクラスの@BeforeClass関数)
  2. テストクラスのコンストラクタ(プロパティの初期化処理、TestRuleのコンストラクタの処理もここで行われる)
  3. TestRule.apply()Statement.evaluate()より前に実行される処理(TestWatcherを実装しているならstarting()
    • InstantTaskExecutorRuleではここでLiveData内部で使うExecutorをテスト用のモックに差し替えている
    • TestCoroutineDispatcherを使ったセットアップではこのタイミングでDispacherをテスト用のモックに差し替える
  4. テストクラスの@Before関数
  5. テストケース

先の例ではテスト対象の初期化はリストの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:個人的にそう呼んでいる