如何对 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() 之后,情况变得更好了。