如何使用带有 runTest 的 Flows 和 LiveData 对 ViewModel 进行单元测试?
How to Unit Test a ViewModel using Flows and LiveData with runTest?
假设我的 ViewModel 看起来像这样:
class ViewModelA(
repository: Repository,
ioCoroutineDispatcher: CoroutineDispatcher,
) : ViewModel() {
val liveData : LiveData<String> = repository.getFooBarFlow().asLiveData(ioCoroutineDispatcher)
}
想象一下我的存储库实现如下所示:
class RepositoryImpl : Repository() {
override fun getFooBarFlow() : Flow<String> = flow {
emit("foo")
delay(300)
emit("bar")
}
}
我如何对 LiveData
立即 发出“foo”这一事实进行单元测试,然后 300 毫秒后(不再,不less),那个“bar”是由 LiveData
?
发出的
您可以使用 JUnit 4 或 5,但您必须使用 Kotlin 1.6、kotlinx-coroutines-test 1.6 和 runTest {}
而不是 runBlockingTest {}
(我用 runBlockingTest {}
测试没有问题)
coroutine-test 1.6 中的新 TestCoroutineRule
如下所示:
@ExperimentalCoroutinesApi
class TestCoroutineRule : TestRule {
val testCoroutineDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
}
}
fun runTest(block: suspend TestScope.() -> Unit) = testScope.runTest { block() }
}
runTest
的行为与 runBlockingTest
有很大不同:
runTest() will automatically skip calls to delay() and handle uncaught exceptions. Unlike runBlockingTest(), it will wait for asynchronous callbacks to handle situations where some code runs in dispatchers that are not integrated with the test module. (source)
请注意,advanceTimeBy(n)
并没有真正将虚拟协程时间提前 n。与1.5版本的区别是它不会执行第n个调度的任务,只执行第n - 1个调度的任务。要在at n 执行任务计划,您需要使用新函数runCurrent()
.
测试的示例实现是:
@ExperimentalCoroutinesApi
class DetailViewModelTest {
@get:Rule
val testCoroutineRule = TestCoroutineRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun getFooBarFlow() = testCoroutineRule.runTest {
// Given
val fakeRepository = object : Repository {
override fun getFooBarFlow(): Flow<String> = flow {
emit("foo")
delay(300)
emit("bar")
}
}
// When
val liveData = ViewModelA(fakeRepository, testCoroutineRule.testCoroutineDispatcher).liveData
liveData.observeForever { }
// Then
runCurrent()
assertEquals("foo", liveData.value)
advanceTimeBy(300)
runCurrent()
assertEquals("bar", liveData.value)
}
}
可以在此处找到带有一些助手的完整实现:https://github.com/NinoDLC/HiltNavArgsDemo/commit/c2d84dd79c846b96d419217eb68dd7e12baedeb6
假设我的 ViewModel 看起来像这样:
class ViewModelA(
repository: Repository,
ioCoroutineDispatcher: CoroutineDispatcher,
) : ViewModel() {
val liveData : LiveData<String> = repository.getFooBarFlow().asLiveData(ioCoroutineDispatcher)
}
想象一下我的存储库实现如下所示:
class RepositoryImpl : Repository() {
override fun getFooBarFlow() : Flow<String> = flow {
emit("foo")
delay(300)
emit("bar")
}
}
我如何对 LiveData
立即 发出“foo”这一事实进行单元测试,然后 300 毫秒后(不再,不less),那个“bar”是由 LiveData
?
您可以使用 JUnit 4 或 5,但您必须使用 Kotlin 1.6、kotlinx-coroutines-test 1.6 和 runTest {}
而不是 runBlockingTest {}
(我用 runBlockingTest {}
测试没有问题)
coroutine-test 1.6 中的新 TestCoroutineRule
如下所示:
@ExperimentalCoroutinesApi
class TestCoroutineRule : TestRule {
val testCoroutineDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
}
}
fun runTest(block: suspend TestScope.() -> Unit) = testScope.runTest { block() }
}
runTest
的行为与 runBlockingTest
有很大不同:
runTest() will automatically skip calls to delay() and handle uncaught exceptions. Unlike runBlockingTest(), it will wait for asynchronous callbacks to handle situations where some code runs in dispatchers that are not integrated with the test module. (source)
请注意,advanceTimeBy(n)
并没有真正将虚拟协程时间提前 n。与1.5版本的区别是它不会执行第n个调度的任务,只执行第n - 1个调度的任务。要在at n 执行任务计划,您需要使用新函数runCurrent()
.
测试的示例实现是:
@ExperimentalCoroutinesApi
class DetailViewModelTest {
@get:Rule
val testCoroutineRule = TestCoroutineRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Test
fun getFooBarFlow() = testCoroutineRule.runTest {
// Given
val fakeRepository = object : Repository {
override fun getFooBarFlow(): Flow<String> = flow {
emit("foo")
delay(300)
emit("bar")
}
}
// When
val liveData = ViewModelA(fakeRepository, testCoroutineRule.testCoroutineDispatcher).liveData
liveData.observeForever { }
// Then
runCurrent()
assertEquals("foo", liveData.value)
advanceTimeBy(300)
runCurrent()
assertEquals("bar", liveData.value)
}
}
可以在此处找到带有一些助手的完整实现:https://github.com/NinoDLC/HiltNavArgsDemo/commit/c2d84dd79c846b96d419217eb68dd7e12baedeb6