如何对 networkBoundResource 的 kotlin-flow 版本进行单元测试?
How to UnitTest kotlin-flow version of networkBoundResource?
我想对我的 AuthRepository
进行单元测试,它正在使用 NetworkBoundResource 的这个“流版本”(找到下面的代码)
我现在的 UnitTest 是这样写的:
@ExperimentalCoroutinesApi
class AuthRepositoryTest {
companion object {
const val FAKE_ID_TOKEN = "FAkE_ID_TOKEN"
}
@get:Rule
val testCoroutineRule = TestCoroutineRule()
private val coroutineDispatcher = TestCoroutineDispatcher()
private val mockUserDao = mockk<UserDao>()
private val mockApiService = mockk<TimetrackerApi>()
private val sut = AuthRepository(
mockUserDao, mockApiService, coroutineDispatcher
)
@Test
fun getAuthToken_noCachedData_shouldMakeNetworkCall() = testCoroutineRule.runBlockingTest {
// Given an empty database
// When getAuthToken is called
sut.getAuthToken(FAKE_ID_TOKEN).first()
coVerify {
// Then first try to fetch data from the DB
mockUserDao.get()
}
}
}
但我实际上会更深入地验证以下逻辑:
coVerifyOrder {
// Then first try to fetch data from the DB
mockUserDao.get()
// Then fetch the User from the API
mockApiService.getUser(FAKE_ID_TOKEN)
// THen insert the user into the DB
mockUserDao.insert(any())
}
但是测试结果告诉我在这种情况下只进行了第一个验证调用 (mockUserDao.get()
)。如果您查看 networkBoundResource
的逻辑(下面的代码),您会发现还应该进行其他两个调用。
在val data = query().first()
之前我也尝试过打假电话。在这种情况下,第一个假电话总是被验证,但之后的一切都没有。
我可以随时提供更多信息,但我认为现在这应该足以开始了解这里发生的事情...
SubjectUnderTest 是我的非常简单AuthRepository
:
class AuthRepository @Inject constructor(
private val userDao: UserDao,
private val apiService: TimetrackerApi,
private val coroutineDispatcher: CoroutineDispatcher
) {
fun getAuthToken(idToken: String): Flow<Resource<User>> {
return networkBoundResource(
query = { userDao.get() },
fetch = { apiService.getUser(idToken) },
saveFetchResult = { apiResponse ->
if (apiResponse is NetworkResponse.Success) {
val user = UserConverter.convert(apiResponse.body)
userDao.insert(user)
}
},
coroutineDispatcher = coroutineDispatcher
)
}
}
这里是networkBoundResource
.
的流版本的代码
inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
coroutineDispatcher: CoroutineDispatcher
) = flow<Resource<ResultType>> {
val data = query().first()
val flow = if (shouldFetch(data)) {
emit(Resource.loading(data))
try {
saveFetchResult(fetch())
query().map { Resource.success(it) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)
query().map { Resource.error(throwable.toString(), it) }
}
} else {
query().map { Resource.success(it) }
}
emitAll(flow)
}.onStart {
emit(Resource.loading(null))
}.catch {
emit(Resource.error("An error occurred while fetching data!", null))
}.flowOn(coroutineDispatcher)
更新
在尝试实施进一步测试时,问题数量增加了。最后我重写了 networkBoundResource
,结果比在 SO 上流传的要好得多。相应的问题和答案连同代码可以在这里找到:
原答案
感谢@aSemy,我被迫深入挖掘。结果是我犯了一系列错误。 None 其中很大,但很难找到。他们在这里:
1。协程断点
在调试和设置断点时,请务必将线程挂起设置为“全部”。这可以通过右键单击断点轻松完成,选择单选按钮“All”并单击“Make default”。以后每一个断点都会有正确的设置。
2。正确捕获流异常
在某些时候,我意识到我已经设置了 .catch
函数,但我没有从中得到任何异常或任何有用的提示。这是因为我忽略了进入该函数的异常参数。将代码更改为
.catch { exception ->
emit(Resource.error("An error occurred while fetching data! $exception", null))
}
是正确调试 networkBoundResource
所需的重要步骤
3。正确设置 UnitTest
的“给定”部分
通过启用例外,我得到的信息是 userDao.get()
return 什么都没有。然后我突然想到,即使假设数据库是空的,这个调用也应该 return 是一个空流。所以我将单元测试的给定部分更改为
// Given an empty database
every { mockUserDao.get() } returns flowOf()
4。修复生产代码
从那时起,我得到了一个有效的 UnitTest,这导致我在生产代码中出现更多错误。正如它应该做的那样 :) 我仍然不是完全绿色,但是在将 val data = query().first()
设置为 firstOrNull()
之后,情况变得更好了。
我想对我的 AuthRepository
进行单元测试,它正在使用 NetworkBoundResource 的这个“流版本”(找到下面的代码)
我现在的 UnitTest 是这样写的:
@ExperimentalCoroutinesApi
class AuthRepositoryTest {
companion object {
const val FAKE_ID_TOKEN = "FAkE_ID_TOKEN"
}
@get:Rule
val testCoroutineRule = TestCoroutineRule()
private val coroutineDispatcher = TestCoroutineDispatcher()
private val mockUserDao = mockk<UserDao>()
private val mockApiService = mockk<TimetrackerApi>()
private val sut = AuthRepository(
mockUserDao, mockApiService, coroutineDispatcher
)
@Test
fun getAuthToken_noCachedData_shouldMakeNetworkCall() = testCoroutineRule.runBlockingTest {
// Given an empty database
// When getAuthToken is called
sut.getAuthToken(FAKE_ID_TOKEN).first()
coVerify {
// Then first try to fetch data from the DB
mockUserDao.get()
}
}
}
但我实际上会更深入地验证以下逻辑:
coVerifyOrder {
// Then first try to fetch data from the DB
mockUserDao.get()
// Then fetch the User from the API
mockApiService.getUser(FAKE_ID_TOKEN)
// THen insert the user into the DB
mockUserDao.insert(any())
}
但是测试结果告诉我在这种情况下只进行了第一个验证调用 (mockUserDao.get()
)。如果您查看 networkBoundResource
的逻辑(下面的代码),您会发现还应该进行其他两个调用。
在val data = query().first()
之前我也尝试过打假电话。在这种情况下,第一个假电话总是被验证,但之后的一切都没有。
我可以随时提供更多信息,但我认为现在这应该足以开始了解这里发生的事情...
SubjectUnderTest 是我的非常简单AuthRepository
:
class AuthRepository @Inject constructor(
private val userDao: UserDao,
private val apiService: TimetrackerApi,
private val coroutineDispatcher: CoroutineDispatcher
) {
fun getAuthToken(idToken: String): Flow<Resource<User>> {
return networkBoundResource(
query = { userDao.get() },
fetch = { apiService.getUser(idToken) },
saveFetchResult = { apiResponse ->
if (apiResponse is NetworkResponse.Success) {
val user = UserConverter.convert(apiResponse.body)
userDao.insert(user)
}
},
coroutineDispatcher = coroutineDispatcher
)
}
}
这里是networkBoundResource
.
inline fun <ResultType, RequestType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> RequestType,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
coroutineDispatcher: CoroutineDispatcher
) = flow<Resource<ResultType>> {
val data = query().first()
val flow = if (shouldFetch(data)) {
emit(Resource.loading(data))
try {
saveFetchResult(fetch())
query().map { Resource.success(it) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)
query().map { Resource.error(throwable.toString(), it) }
}
} else {
query().map { Resource.success(it) }
}
emitAll(flow)
}.onStart {
emit(Resource.loading(null))
}.catch {
emit(Resource.error("An error occurred while fetching data!", null))
}.flowOn(coroutineDispatcher)
更新
在尝试实施进一步测试时,问题数量增加了。最后我重写了 networkBoundResource
,结果比在 SO 上流传的要好得多。相应的问题和答案连同代码可以在这里找到:
原答案
感谢@aSemy,我被迫深入挖掘。结果是我犯了一系列错误。 None 其中很大,但很难找到。他们在这里:
1。协程断点
在调试和设置断点时,请务必将线程挂起设置为“全部”。这可以通过右键单击断点轻松完成,选择单选按钮“All”并单击“Make default”。以后每一个断点都会有正确的设置。
2。正确捕获流异常
在某些时候,我意识到我已经设置了 .catch
函数,但我没有从中得到任何异常或任何有用的提示。这是因为我忽略了进入该函数的异常参数。将代码更改为
.catch { exception ->
emit(Resource.error("An error occurred while fetching data! $exception", null))
}
是正确调试 networkBoundResource
3。正确设置 UnitTest
的“给定”部分通过启用例外,我得到的信息是 userDao.get()
return 什么都没有。然后我突然想到,即使假设数据库是空的,这个调用也应该 return 是一个空流。所以我将单元测试的给定部分更改为
// Given an empty database
every { mockUserDao.get() } returns flowOf()
4。修复生产代码
从那时起,我得到了一个有效的 UnitTest,这导致我在生产代码中出现更多错误。正如它应该做的那样 :) 我仍然不是完全绿色,但是在将 val data = query().first()
设置为 firstOrNull()
之后,情况变得更好了。