viewModelScopeを使っている関数を持つViewModel
次のRecommendBookViewModelクラスは、loadRecommendBooks
関数という内部でviewModelScopeを用いて非同期処理を行う関数を持っている。
import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import hoge.fuga.recommendbook.RecommendBook import hoge.fuga.recommendbook.RecommendBookRepository data class ViewState( val loading: Boolean, val recommendBooks: List<RecommendBook> ) class RecommendBookViewModel( private val recommendBookRepository: RecommendBookRepository ) : ViewModel() { private val _viewState = MutableStateFlow(ViewState(true, emptyList())) val viewState: StateFlow<ViewState> = _viewState fun loadRecommendBooks() { viewModelScope.launch { val recommendBooks = recommendBookRepository.recommendBooks() _viewState.value = ViewState(false, recommendBooks) } } }
loadRecommendBooks
関数をいい感じにテストするためにはいくらかの準備が必要になる。
環境
次のような環境とする。
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.21" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.1" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.6.1" testImplementation 'org.robolectric:robolectric:4.4' // 多分今回のテストには関係ない testImplementation 'androidx.test:runner:1.3.0' testImplementation 'androidx.test.ext:junit:1.1.2' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2' testImplementation "io.mockk:mockk:1.10.2" testImplementation('androidx.test.ext:truth:1.3.0') { exclude group: 'com.google.auto.value', module: 'auto-value-annotations' }
TestCoroutinesExtensionを用意する
Junit5ではorg.junit.rules.TestRule
は無くなった。代わりにorg.junit.jupiter.api.extension
パッケージ内の各種インタフェースを実装したクラスを、ExtendWith
で指定する形になった。そこでDispatchers.setMain()
などを実行するTestCoroutinesExtensionを作る。
package hoge.fuga import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain import org.junit.jupiter.api.extension.* // テストの前後の実行、テストのコンストラクタへのパラメータをサポートする class TestCoroutinesExtension : AfterTestExecutionCallback, BeforeTestExecutionCallback, ParameterResolver { private val namespace = ExtensionContext.Namespace.create(javaClass) private val key: Any = TestCoroutineDispatcher::class.java private val dispatcher = TestCoroutineDispatcher() override fun afterTestExecution(context: ExtensionContext?) { Dispatchers.resetMain() } override fun beforeTestExecution(context: ExtensionContext?) { Dispatchers.setMain(dispatcher) } override fun supportsParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Boolean { return parameterContext.parameter.type === TestCoroutineDispatcher::class.java } override fun resolveParameter(parameterContext: ParameterContext, extensionContext: ExtensionContext): Any { return extensionContext.getStore(namespace).getOrComputeIfAbsent(key, { dispatcher }, TestCoroutineDispatcher::class.java) } }
ViewModelのテストを書く
TestCoroutinesExtensionを使ってRecommendBookViewModel
のテストを書く。
import com.google.common.truth.Truth.assertThat import io.mockk.coEvery import io.mockk.mockk import hoge.fuga.TestCoroutinesExtension import hoge.fuga.recommendbook.RecommendBook import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith // エクステンションを使う @ExtendWith(TestCoroutinesExtension::class) class RecommendBookViewModelTest(val dispatcher: TestCoroutineDispatcher) { // コンストラクタでTestCoroutineDispatcherを受け取る↑ @Nested inner class loadRecommendBooks { @Test fun `success`() = dispatcher.runBlockingTest { val viewModel = RecommendBookViewModel( mockk { coEvery { recommendBooks() } returns listOf( RecommendBook("a"), RecommendBook("b"), RecommendBook("c") ) } ) viewModel.loadRecommendBooks() val viewState = viewModel.viewState.value assertThat(viewState.loading).isFalse() assertThat(viewState.recommendBooks.size).isEqualTo(3) } } }
割とシンプルにできて良さそう。