我正在使用Mockito,junit5和协程在存储库中获取数据。但是在测试用例中调用了no方法。我尝试使用没有任何Dispatchers
和emit()
函数的普通暂停函数,并且它可以正常工作。因此,我想原因可能是由于实时数据协程
GitReposRepository.kt
fun loadReposSuspend(owner: String) = liveData(Dispatchers.IO) {
emit(Result.Loading)
val response = githubService.getReposNormal(owner)
val repos = response.body()!!
if (repos.isEmpty()) {
emit(Result.Success(repos))
repoDao.insert(*repos.toTypedArray())
} else {
emitSource(repoDao.loadRepositories(owner)
.map { Result.Success(it) })
}
}
GitReposRepositoryTest.kt
internal class GitRepoRepositoryTest {
private lateinit var appExecutors:AppExecutors
private lateinit var repoDao: RepoDao
private lateinit var githubService: GithubService
private lateinit var gitRepoRepository: GitRepoRepository
@BeforeEach
internal fun setUp() {
appExecutors = mock(AppExecutors::class.java)
repoDao = mock(RepoDao::class.java)
githubService = mock(GithubService::class.java)
gitRepoRepository = GitRepoRepository(appExecutors,
repoDao,
githubService)
}
@Test
internal fun `should call network to fetch result and insert to db`() = runBlocking {
//given
val owner = "Testing"
val response = Response.success(listOf(Repo(),Repo()))
`when`(githubService.getReposNormal(ArgumentMatchers.anyString())).thenReturn(response)
//when
gitRepoRepository.loadReposSuspend(owner)
//then
verify(githubService).getReposNormal(owner)
verify(repoDao).insertRepos(ArgumentMatchers.anyList())
}
}
答案 0 :(得分:1)
几天后在互联网上搜索。我发现了如何在实时数据中使用协程进行单元测试,并提出以下想法。这可能不是最好的主意,但希望它可以为遇到类似问题的人们带来一些见识。
使用实时数据进行协程单元测试的必要部分很少:
需要为单元测试(Coroutine Rule, InstantExecutor Rule)添加2条规则。如果像我一样使用Junit5,则应改用扩展名。协程规则为您提供了在 Java UnitTest 中使用testCoroutine调度程序的功能。 InstantExecutor Rule为您提供监视 Java UnitTest 中实时数据发出值的功能。并且请注意read_exact
是 Java UnitTest 中测试协程的最重要部分。建议观看有关在Kotlin https://youtu.be/KMb0Fs8rCRs
需要设置要在构造函数中注入的CoroutineDispatcher
您应该始终注入调度程序 (https://youtu.be/KMb0Fs8rCRs?t=850)
一些针对livedata的livedata扩展,可帮助您验证来自实时数据的发射值的值。
这是我的存储库(我在android官方中遵循recommended app architecture)
GitRepoRepository.kt(此想法来自两个来源,LegoThemeRepository,NetworkBoundResource
coroutine.dispatcher
GitRepoRepositoryTest.kt
@Singleton
class GitRepoRepository @Inject constructor(private val appExecutors: AppExecutors,
private val repoDao: RepoDao,
private val githubService: GithubService,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val repoListRateLimit: RateLimiter<String> = RateLimiter(
10,
TimeUnit.MINUTES)
) {
fun loadRepo(owner: String
): LiveData<Result<List<Repo>>> = repositoryLiveData(
localResult = { repoDao.loadRepositories(owner) },
remoteResult = {
transformResult { githubService.getRepo(owner) }.apply {
if (this is Result.Error) {
repoListRateLimit.reset(owner)
}
}
},
shouldFetch = { repoListRateLimit.shouldFetch(owner) },
saveFetchResult = { repoDao.insertRepos(it) },
dispatcher = this.dispatcher
)
...
}
CoroutineUtil.kt(想法也来自here,如果您要记录一些信息,请使用自定义实现,以下测试案例为您提供了一些如何在协程中对其进行测试的见解
@ExperimentalCoroutinesApi
@ExtendWith(InstantExecutorExtension::class)
class GitRepoRepositoryTest {
// Set the main coroutines dispatcher for unit testing
companion object {
@JvmField
@RegisterExtension
var coroutinesRule = CoroutinesTestExtension()
}
private lateinit var appExecutors: AppExecutors
private lateinit var repoDao: RepoDao
private lateinit var githubService: GithubService
private lateinit var gitRepoRepository: GitRepoRepository
private lateinit var rateLimiter: RateLimiter<String>
@BeforeEach
fun setUp() {
appExecutors = mock(AppExecutors::class.java)
repoDao = mock(RepoDao::class.java)
githubService = mock(GithubService::class.java)
rateLimiter = mock(RateLimiter::class.java) as RateLimiter<String>
gitRepoRepository = GitRepoRepository(appExecutors,
repoDao,
githubService,
coroutinesRule.dispatcher,
rateLimiter)
}
@Test
fun `should not call network to fetch result if the process in rate limiter is not valid`() = coroutinesRule.runBlocking {
//given
val owner = "Tom"
val response = Response.success(listOf(Repo(), Repo()))
`when`(githubService.getRepo(anyString())).thenReturn(
response)
`when`(rateLimiter.shouldFetch(anyString())).thenReturn(false)
//when
gitRepoRepository.loadRepo(owner).getOrAwaitValue()
//then
verify(githubService, never()).getRepo(owner)
verify(repoDao, never()).insertRepos(anyList())
}
@Test
fun `should reset ratelimiter if the network response contains error`() = coroutinesRule.runBlocking {
//given
val owner = "Tom"
val response = Response.error<List<Repo>>(500,
"Test Server Error".toResponseBody(
"text/plain".toMediaTypeOrNull()))
`when`(githubService.getRepo(anyString())).thenReturn(
response)
`when`(rateLimiter.shouldFetch(anyString())).thenReturn(true)
//when
gitRepoRepository.loadRepo(owner).getOrAwaitValue()
//then
verify(rateLimiter, times(1)).reset(owner)
}
}
CoroutineUtilKtTest.kt
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
object Loading : Result<Nothing>()
data class Error<T>(val message: String) : Result<T>()
object Finish : Result<Nothing>()
}
fun <T, A> repositoryLiveData(localResult: (() -> LiveData<T>) = { MutableLiveData() },
remoteResult: (suspend () -> Result<A>)? = null,
saveFetchResult: suspend (A) -> Unit = { Unit },
dispatcher: CoroutineDispatcher = Dispatchers.IO,
shouldFetch: () -> Boolean = { true }
): LiveData<Result<T>> =
liveData(dispatcher) {
emit(Result.Loading)
val source: LiveData<Result<T>> = localResult.invoke()
.map { Result.Success(it) }
emitSource(source)
try {
remoteResult?.let {
if (shouldFetch.invoke()) {
when (val response = it.invoke()) {
is Result.Success -> {
saveFetchResult(response.data)
}
is Result.Error -> {
emit(Result.Error<T>(response.message))
emitSource(source)
}
else -> {
}
}
}
}
} catch (e: Exception) {
emit(Result.Error<T>(e.message.toString()))
emitSource(source)
} finally {
emit(Result.Finish)
}
}
suspend fun <T> transformResult(call: suspend () -> Response<T>): Result<T> {
try {
val response = call()
if (response.isSuccessful) {
val body = response.body()
if (body != null) return Result.Success(body)
}
return error(" ${response.code()} ${response.message()}")
} catch (e: Exception) {
return error(e.message ?: e.toString())
}
}
fun <T> error(message: String): Result<T> {
return Result.Error("Network call has failed for a following reason: $message")
}
LiveDataTestUtil.kt(这个想法来自aac sample,kotlin-coroutine)
interface Delegation {
suspend fun remoteResult(): Result<String>
suspend fun saveResult(s: String)
fun localResult(): MutableLiveData<String>
fun shouldFetch(): Boolean
}
fun <T> givenSuspended(block: suspend () -> T) = BDDMockito.given(runBlocking { block() })
@ExperimentalCoroutinesApi
@ExtendWith(InstantExecutorExtension::class)
class CoroutineUtilKtTest {
// Set the main coroutines dispatcher for unit testing
companion object {
@JvmField
@RegisterExtension
var coroutinesRule = CoroutinesTestExtension()
}
val delegation: Delegation = mock()
private val LOCAL_RESULT = "Local Result Fetch"
private val REMOTE_RESULT = "Remote Result Fetch"
private val REMOTE_CRASH = "Remote Result Crash"
@BeforeEach
fun setUp() {
given { delegation.shouldFetch() }
.willReturn(true)
given { delegation.localResult() }
.willReturn(MutableLiveData(LOCAL_RESULT))
givenSuspended { delegation.remoteResult() }
.willReturn(Result.Success(REMOTE_RESULT))
}
@Test
fun `should call local result only if the remote result should not fetch`() = coroutinesRule.runBlocking {
//given
given { delegation.shouldFetch() }.willReturn(false)
//when
repositoryLiveData<String, String>(
localResult = { delegation.localResult() },
remoteResult = { delegation.remoteResult() },
shouldFetch = { delegation.shouldFetch() },
dispatcher = coroutinesRule.dispatcher
).getOrAwaitValue()
//then
verify(delegation, times(1)).localResult()
verify(delegation, never()).remoteResult()
}
@Test
fun `should call remote result and then save result`() = coroutinesRule.runBlocking {
//when
repositoryLiveData<String, String>(
shouldFetch = { delegation.shouldFetch() },
remoteResult = { delegation.remoteResult() },
saveFetchResult = { s -> delegation.saveResult(s) },
dispatcher = coroutinesRule.dispatcher
).getOrAwaitValue()
//then
verify(delegation, times(1)).remoteResult()
verify(delegation,
times(1)).saveResult(REMOTE_RESULT)
}
@Test
fun `should emit Loading, Success, Finish Status when we fetch local and then remote`() = coroutinesRule.runBlocking {
//when
val ld = repositoryLiveData<String, String>(
localResult = { delegation.localResult() },
shouldFetch = { delegation.shouldFetch() },
remoteResult = { delegation.remoteResult() },
saveFetchResult = { delegation.shouldFetch() },
dispatcher = coroutinesRule.dispatcher
)
//then
ld.captureValues {
assertEquals(arrayListOf(Result.Loading,
Result.Success(LOCAL_RESULT),
Result.Finish), values)
}
}
@Test
fun `should emit Loading,Success, Error, Success, Finish Status when we fetch remote but fail`() = coroutinesRule.runBlocking {
givenSuspended { delegation.remoteResult() }
.willThrow(RuntimeException(REMOTE_CRASH))
//when
val ld = repositoryLiveData<String, String>(
localResult = { delegation.localResult() },
shouldFetch = { delegation.shouldFetch() },
remoteResult = { delegation.remoteResult() },
saveFetchResult = { delegation.shouldFetch() },
dispatcher = coroutinesRule.dispatcher
)
//then
ld.captureValues {
assertEquals(arrayListOf(Result.Loading,
Result.Success(LOCAL_RESULT),
Result.Error(REMOTE_CRASH),
Result.Success(LOCAL_RESULT),
Result.Finish
), values)
}
}
}