如何对 returns livedata 的函数进行单元测试
how to unit test a function that returns livedata
在我的 viewModel 中,我有一个函数 returns liveData。该函数直接在片段中调用,因此可以直接在那里观察到。我不知道如何测试这个函数,因为在测试的情况下没有观察到函数发出的 liveData,因此它不会 return 值。
这是我的函数,我想为以下对象编写测试:
fun saveRating(rating: Float, eventName: String): LiveData<Response<SaveRatingData?>?> {
val request = RatingRequest(rating.toDouble(), eventName, false)
return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emit(repository.saveRatings(request))
}
}
这就是我在片段中的调用方式:
viewModel.saveRating(rating, npsEventData?.eventName ?: "").observe(this, Observer {
// on getting data
})
提前致谢!
您需要有一个 testCoroutineDispatcher 或 testCoroutineScope 才能将您的 viewModel 的范围设置为测试范围。
class TestCoroutineRule : TestRule {
private val testCoroutineDispatcher = TestCoroutineDispatcher()
val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest { block() }
}
任何官方 kotlin 或 Android 文档中均未提及 Try-catch 块,但测试异常会导致异常,而不是像我在此 .
中要求的那样通过测试
我在 testCoroutineDispatcher 中遇到的另一件事是调度程序不足以通过某些测试,您需要将 coroutineScope 而不是调度程序注入 viewModel。
例如
fun throwExceptionInAScope(coroutineContext: CoroutineContext) {
viewModelScope.launch(coroutineContext) {
delay(2000)
throw RuntimeException("Exception Occurred")
}
}
你有一个像这样的函数抛出异常,你将 testCoroutineContext 传递给这个测试它失败了。
@Test(expected = RuntimeException::class)
fun `Test function that throws exception`() =
testCoroutineDispatcher.runBlockingTest {
// Using testCoroutineDispatcher causes this test to FAIL
viewModel.throwExceptionInAScope(testCoroutineDispatcher.coroutineContext)
// This one passes since we use context of current coroutineScope
viewModel.throwExceptionInAScope(this.coroutineContext)
}
使用classMyViewModel(private val coroutineScope: CoroutineScope)
即可通过
现在,让我们来看看如何使用异步任务测试 liveData。我用这个 class, Google 的 LiveDataTestUtil
class, 来同步 liveData
和
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
作为规则
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
this.removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
/**
* Observes a [LiveData] until the `block` is done executing.
*/
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
现在,您可以像测试同步代码一样对其进行测试
@Test
fun `Given repo saves response, it should return the correct one` = testCoroutineScope.runBlockingTest {
// GIVEN
val repository = mockk<<Repository>()
val actual = Response(...)
coEvery { repository.saveRatings } returns actual
// WHEN
val expected = viewModel.saveResponse()
// THEN
Truth.assertThat(actual).isEqualTo(expected)
}
我使用了 mockK,它与挂起模拟配合得很好。
此外,如果您有改装或房间功能调用,则不需要使用 Dispatchers.IO
,如果您不执行改装或房间操作以外的其他任务,它们会使用带有挂起修饰符的自己的线程。
在我的 viewModel 中,我有一个函数 returns liveData。该函数直接在片段中调用,因此可以直接在那里观察到。我不知道如何测试这个函数,因为在测试的情况下没有观察到函数发出的 liveData,因此它不会 return 值。
这是我的函数,我想为以下对象编写测试:
fun saveRating(rating: Float, eventName: String): LiveData<Response<SaveRatingData?>?> {
val request = RatingRequest(rating.toDouble(), eventName, false)
return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emit(repository.saveRatings(request))
}
}
这就是我在片段中的调用方式:
viewModel.saveRating(rating, npsEventData?.eventName ?: "").observe(this, Observer {
// on getting data
})
提前致谢!
您需要有一个 testCoroutineDispatcher 或 testCoroutineScope 才能将您的 viewModel 的范围设置为测试范围。
class TestCoroutineRule : TestRule {
private val testCoroutineDispatcher = TestCoroutineDispatcher()
val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
override fun apply(base: Statement, description: Description?) = object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
Dispatchers.setMain(testCoroutineDispatcher)
base.evaluate()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
}
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
testCoroutineScope.runBlockingTest { block() }
}
任何官方 kotlin 或 Android 文档中均未提及 Try-catch 块,但测试异常会导致异常,而不是像我在此
我在 testCoroutineDispatcher 中遇到的另一件事是调度程序不足以通过某些测试,您需要将 coroutineScope 而不是调度程序注入 viewModel。
例如
fun throwExceptionInAScope(coroutineContext: CoroutineContext) {
viewModelScope.launch(coroutineContext) {
delay(2000)
throw RuntimeException("Exception Occurred")
}
}
你有一个像这样的函数抛出异常,你将 testCoroutineContext 传递给这个测试它失败了。
@Test(expected = RuntimeException::class)
fun `Test function that throws exception`() =
testCoroutineDispatcher.runBlockingTest {
// Using testCoroutineDispatcher causes this test to FAIL
viewModel.throwExceptionInAScope(testCoroutineDispatcher.coroutineContext)
// This one passes since we use context of current coroutineScope
viewModel.throwExceptionInAScope(this.coroutineContext)
}
使用classMyViewModel(private val coroutineScope: CoroutineScope)
现在,让我们来看看如何使用异步任务测试 liveData。我用这个 class, Google 的 LiveDataTestUtil
class, 来同步 liveData
和
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
作为规则
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
this.removeObserver(observer)
throw TimeoutException("LiveData value was never set.")
}
@Suppress("UNCHECKED_CAST")
return data as T
}
/**
* Observes a [LiveData] until the `block` is done executing.
*/
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
现在,您可以像测试同步代码一样对其进行测试
@Test
fun `Given repo saves response, it should return the correct one` = testCoroutineScope.runBlockingTest {
// GIVEN
val repository = mockk<<Repository>()
val actual = Response(...)
coEvery { repository.saveRatings } returns actual
// WHEN
val expected = viewModel.saveResponse()
// THEN
Truth.assertThat(actual).isEqualTo(expected)
}
我使用了 mockK,它与挂起模拟配合得很好。
此外,如果您有改装或房间功能调用,则不需要使用 Dispatchers.IO
,如果您不执行改装或房间操作以外的其他任务,它们会使用带有挂起修饰符的自己的线程。