忘れそうになるのでメモ。テストは Kotest をつかっているが、極力 JUnit でのテストの記述と対比しやすいよう StringSpec
で書いていく。
最近 RxJava から Kotlin Coroutines への乗り換えを進めていて、テストの書き方がどう異なるか(あるいは同様となるのか)についてまとめたくなったので残している。
RxJava
次の SomeModel
をテストしたい。
class SomeModel { private val textMutation: BehaviorSubject<String> = BehabiorSubject.createDefault("foo") val text: Subject<String> = textMutation.hide() fun changeText(text: String) { textMutation.onNext(text) } }
changeText
関数によるテキストの変更を text
を観測して確かめる。
RxJava では TestObserver
があるのでこれを使う。
class SomeModelTest : StringSpec() { init { "Change text" { // Given: SomeModel val model = SomeModel() val observer: TestObserver<String> = model.text.test() // When: Change text model.changeText("bar") // Then: Observe 2 updates observer.awaitCount(2).assertValueCount(2) // And: "foo" comes first observer.assertValueAt(0) { text -> text == "foo" } // And: "bar" comes second observer.assertValueAt(1) { text -> text == "bar" } } } }
Kotlin Coroutines
RxJava の例と似たようなクラスをテストしてみる。RxJava では BehaviorSubject
をつかっていたが、Kotlin Coroutines では StateFlow
を使う。
class SomeModel { private val textMutation: MutableStateFlow<String> = MutableStateFlow("foo") val text: StateFlow<String> = textMutation.asStateFlow() fun changeText(text: String) { textMutation.value = text } }
SomeModel
のテストは RxJava の例と同じく、changeText
関数によるテキストの変更を text
を観測して確かめる。
RxJava の TestObserver
に相当するものとして、StateFlow
の通知する値を詰め込んでおく ArrayList
を用意する。
ただし kotlinx-coroutines のバージョンによってテストの書き方が変わることに注意する。
次の例は 1.5.2 以前の書き方。
class SomeModelTest : StringSpec() { init { "Change text" { runBlockingTest { // Given: SomeModel val model = SomeModel() val observedChanges: MutableList<String> = ArrayList() val job = launch { model.text.toList(observedChanges) } // When: Change text model.changeText("bar") // And: Tear down job job.cancel() // Then: Observe 2 updates observedChanges.size.shouldBe(2) // And: "foo" comes first observedChanges[0].shouldBe("foo") // And: "bar" comes second observedChanges[1].shouldBe("bar") } } } }
次の例は 1.6.0 以降の書き方。
runBlockingTest
が deprecated となり、代わりに runTest
を使う。これにともなって、テストの前準備として StandardTestDispatcher
を Dispatchers.setMain
にわたすことと、runTest
ブロック内で launch
するときに UnconfinedTestDispatcher
を利用して StateFlow
が通知する全ての値を観測可能にすることが必要になる。
この変更についてはマイグレーションガイドが提供されている。
class SomeModelTest : StringSpec() { init { "Change text" { runTest { // Given: SomeModel val model = SomeModel() val observedChanges: MutableList<String> = ArrayList() val job = launch(UnconfinedTestDispatcher()) { model.text.toList(observedChanges) } // When: Change text model.changeText("bar") // And: Tear down job job.cancel() // Then: Observe 2 updates observedChanges.size.shouldBe(2) // And: "foo" comes first observedChanges[0].shouldBe("foo") // And: "bar" comes second observedChanges[1].shouldBe("bar") } } } // Robolectric を利用したテストを記述するとき、StandardTestDispatcher の設定は必ず beforeTest で実行する //(beforeSpec では Looper クラスが見つからず失敗する) override fun beforeTest(testCase: TestCase) { super.beforeTest(testCase) Dispatchers.setMain(StandardTestDispatcher()) } override fun afterTest(testCase: TestCase, result: TestResult) { Dispatchers.resetMain() super.afterTest(testCase, result) } }