Infinito Nirone 7

白羽の矢を刺すスタイル

Android アプリのモジュールで kotest を使いユニットテストを記述する

以前は Kotlintest と呼ばれたテストフレームワークが Kotest と名前を変えて開発が進められています。

github.com

このテストフレームワークは非常に多様なテストの記述方法をサポートしており、テストケースのスタイルからアサーションメソッド、パラメトライズドテストのサポートまでテストに必要な幅広い関心をカバーしています。 マルチプラットフォームでの動作をサポートしたテストフレームワークとなっていて、自分で拡張を導入するための構造も持っています。公式にも様々な拡張が提供されていて、Robolectric と一緒にテストを動作させるための拡張も用意されています。 ただし2020年10月現在 Robolectric の拡張は experimental であり、うまく動作しないパターンが多々あります。この記事では、次に示す各ライブラリのバージョンを組み合わせて kotest を Android アプリプロジェクトに導入する方法を紹介します。

  • kotest: 4.2.5
  • kotest-robolectric-extension: 4.0.1
  • Robolectric: 4.4

依存関係

build.gradle に下記の記述を加え、kotest および Robolectric を導入します。

android{
  testOptions {
    unitTests.all {
      useJUnitPlatform()
    }
  }
}

dependencies {
  // kotest
  testImplementation 'io.kotest:kotest-runner-junit5:4.2.5'
  testImplementation 'io.kotest:kotest-assertions-core:4.2.5'
  testImplementation 'io.kotest:kotest-property:4.2.5'
  testImplementation 'io.kotest:kotest-extensions-robolectric-jvm:4.0.1'

  // robolectric
  testImplementation 'org.robolectric:robolectric:4.4'
}

必要に応じて、テストの source directory 配下に resources ディレクトリを掘って、robolectric.properties ファイルを作ります。

sdk=28
application=your.app.pkg.YourApp

kotest-robolectric-extension による Robolectric の有効化

テストの source directory の好きなパッケージに ProjectConfig オブジェクトを作ります。

ProjectConfig オブジェクトは様々な extension を追加したり、テストの並列数を制御したりするためのオブジェクトです。 Robolectric を kotest で動作できるようにするには、extensions で RobolectricExtension を含むリストを返します。

また、Robolectric を使う場合の制約として、IsolationMode を InstancePerLeaf にしておく必要があります。IsolationMode はテストケースのインスタンスをどの単位でもつかを制御するパラメータで、InstancePerLeaf がもっとも細かい単位でインスタンスを作ります。現状の kotest robolectric extension による Robolectric の初期化処理では、モジュール内に複数のテストを記述したファイルがある場合、Looper の初期化処理が複数回呼ばれてしまい例外が投げられることがあります。IDE では個別にテストを実行するため気付きにくいですが、CLI で一気にテストを実行するとこのエラーに遭遇します。InstancePerLeaf ではテストケースのインスタンスごとに Robolectric の設定も異なる扱いをうけるためか、この初期化処理の重複による失敗を回避できます。

object ProjectConfig : AbstractProjectConfig() {

  override val isolationMode: IsolationMode = InstancePerLeaf

  override fun extensions(): List<Extension> = listOf(RobolectricExtension())
}

ちなみに、筆者が試した限り Robolectric を有効化したモジュールで ProjectConfig の parallelism を 2 以上にした場合も Robolectric の内部状態が不正になりテストが実行できなくなりました。

テストの記述

kotest-robolectric-extension で Robolectric を有効化したモジュールでは、自分が確認した限り次のテストスタイルが利用可能です。これら以外を使用すると Robolectric の初期化処理が複数回呼ばれてテストが失敗します(とくに BehaviorSpecDescribeSpecFeatureSpec)。

  • StringSpec
  • WordSpec
  • FunSpec

Robolectric を利用するテストには @RobolectricTest アノテーションを付けます。 が、現状の kotest-robolectric-extension では同じモジュール内で Robolectric を利用しなくてもよいテストを記述している場合でも、@RobolectricTest アノテーションを付けておかないとテストが実行できない(テストケースがひとつも実行されず正常終了する)ため、Robolectric が不要なテストにも @RobolectricTest アノテーションを付与することになります。

@RobolectricTest
class SomeTest : StringSpec() {
  init {
    "something" {
      // Given: Values
      val a = 0
      val b = 1

      // When: Subtract
      val subtracted = a - b

      // Then: subtracted value should be -1
      subtracted.shouldBe(-1)
    }
  }
}

マルチモジュールプロジェクトでの kotest の設定

先に紹介した ProjectConfig はモジュールごとに定義します。マルチモジュールな状況では kotest を使うすべてのモジュールで ProjectConfig オブジェクトを作る必要があります。少々面倒ではありますが、これにより、Robolectric を使うモジュールとそうでないモジュールを切り分けることができるため、Robolectric が必要なモジュールでは StringSpec を用い、そうでないモジュールでは BehaviorSpec を用いるなどのスタイルの使い分けがしやすくなります。