ViewModel 单元测试(JUnit5、CoroutineDispatcher、Turbine、Mockk)未按预期工作

问题描述 投票:0回答:1

我正在尝试了解 Turbine 如何与

StateFlow
一起工作。

HelloWorldView模型

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class HelloWorldViewModel(
    private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
    private val doOperationUseCase: DoOperationUseCase
) : ViewModel() {

    private val _state: MutableStateFlow<HelloWorldState> = MutableStateFlow(HelloWorldState())
    val state: StateFlow<HelloWorldState> = _state.asStateFlow()

    fun doOperation() {
        viewModelScope.launch(dispatcher) {
            _state.emit(state.value.copy(loading = true))
            doOperationUseCase()
            _state.emit(state.value.copy(loading = false))
        }
    }

}

你好世界状态

data class HelloWorldState(val loading: Boolean = false)

DoOperationUseCase

class DoOperationUseCase {
    suspend operator fun invoke(): Result<List<String>> {
        delay(500)
        return Result.success(listOf("1"))
    }
}

HelloWorldViewModel测试

import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(MockKExtension::class)
class HelloWorldViewModelTest {

    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()

    @MockK
    lateinit var doOperationUseCase: DoOperationUseCase

    private lateinit var classUnderTest: HelloWorldViewModel

    @BeforeEach
    fun setUp() {
        Dispatchers.setMain(testDispatcher)
        classUnderTest = HelloWorldViewModel(testDispatcher, doOperationUseCase)
    }

    @AfterEach
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun doOperation() = runTest {

        coEvery { doOperationUseCase() } returns Result.success(listOf(""))
        assertEquals(HelloWorldState(), classUnderTest.state.value) // expected for initial state
        classUnderTest.state.test {
            classUnderTest.doOperation()
            assertEquals(classUnderTest.state.value.copy(loading = true), awaitItem()) // expected before calling doOperationUseCase use case
            assertEquals(classUnderTest.state.value.copy(loading = false), awaitItem()) // expected after calling doOperationUseCase use case
        }
    }
}

结果:

No value produced in 3s
app.cash.turbine.TurbineAssertionError: No value produced in 3s

仅在以下情况下测试成功:

@Test
fun doOperation() = runTest {

    coEvery { doOperationUseCase() } returns Result.success(listOf(""))

    classUnderTest.state.test {
        classUnderTest.doOperation()
        assertEquals(classUnderTest.state.value.copy(loading = false), awaitItem())
    }
}

依赖关系:

testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
testImplementation("org.junit.platform:junit-platform-console:1.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2")
testImplementation("app.cash.turbine:turbine:1.0.0")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("com.google.truth:truth:1.1.4")

Google 文档没有帮助,因为存储库控制何时发出新值。

android unit-testing junit5 kotlin-stateflow turbine
1个回答
0
投票

您可以创建自定义范围来帮助编写测试用例,而不是使用默认的

viewModelScope
扩展属性。

这是上面代码中问题的解决方案。

步骤1

为 ViewModel 创建自定义可关闭范围

import java.io.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.CoroutineContext

class CloseableCoroutineScope(context: CoroutineContext) : Closeable,    CoroutineScope {
  override val coroutineContext: CoroutineContext = context
  override fun close() {
    coroutineContext.cancel()
  }
}

注意:我是直接从aac ViewModel.kt extpackage androidx.lifecycle

复制的

第2步

在 ViewModel 中使用此自定义范围

class HelloWorldViewModel(
  private val customScope: CloseableCoroutineScope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
  private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
  private val doOperationUseCase: DoOperationUseCase
): ViewModel(customScope) {
  private val _state: MutableStateFlow<HelloWorldState> = MutableStateFlow(HelloWorldState())
  val state: StateFlow<HelloWorldState> = _state.asStateFlow()

  fun doOperation() {
    customScope.launch(dispatcher) {
      _state.emit(state.value.copy(loading = true))
      doOperationUseCase()
      _state.emit(state.value.copy(loading = false))
    }
  }
}

第3步

然后更新您的测试以使用测试范围和调度程序

@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(MockKExtension::class)
class HelloWorldViewModelTest {
  private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
  var doOperationUseCase = DoOperationUseCase()
  private lateinit var classUnderTest: HelloWorldViewModel

  @Test
  fun doOperation() = runTest(testDispatcher) {
    classUnderTest = HelloWorldViewModel(CloseableCoroutineScope(this.coroutineContext), testDispatcher, doOperationUseCase)
    assertEquals(HelloWorldState(), classUnderTest.state.value) // expected for initial state
    classUnderTest.doOperation()
    classUnderTest.state.test {
      assertEquals(classUnderTest.state.value.copy(loading = true), awaitItem()) // expected before calling doOperationUseCase use case
      assertEquals(classUnderTest.state.value.copy(loading = false), awaitItem()) // expected after calling doOperationUseCase use case
    }
  }
}

我已经测试了这个解决方案并且它有效。 附加提示:

  • 我还建议使用 test doubles 而不是模拟。这意味着,您可以为 DoOperationUseCase 创建一个接口并创建一个假实现。
  • 您可以使用动态注入来创建范围并注入到 ViewModel
  • 欲了解更多信息,您可以查看这篇文章
© www.soinside.com 2019 - 2024. All rights reserved.