我正在使用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())
}
}
几天后在互联网上搜索。我了解了如何在实时数据中使用协程进行单元测试,并提出以下想法。这可能不是最好的主意,但希望它可以为遇到类似问题的人们带来一些见识。
使用实时数据进行协程单元测试的必要部分很少:
需要添加2条规则进行单元测试(Coroutine Rule, InstantExecutor Rule)。如果像我一样使用Junit5,则应该改用扩展名。协程规则为您提供使用Java UnitTest中的testCoroutine调度程序的功能。 InstantExecutor Rule为您提供监视Java UnitTest中的实时数据发射值的功能。并且要注意coroutine.dispatcher
是Java UnitTest中测试协程的最重要部分。建议在Kotlin中观看有关协程测试的视频https://youtu.be/KMb0Fs8rCRs
需要设置要在构造函数中注入的CoroutineDispatcher
您应该总是注入调度员(https://youtu.be/KMb0Fs8rCRs?t=850)
用于livedata的一些livedata扩展名,可帮助您验证来自实时数据的发射值的值。
这是我的存储库(我遵循android官方的recommended app architecture)
GitRepoRepository.kt(此想法来自2个来源,LegoThemeRepository,NetworkBoundResource
@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
)
...
}
GitRepoRepositoryTest.kt
@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)
}
}
CoroutineUtil.kt(想法也来自here,如果您想记录一些信息,这里应该是自定义实现,下面的测试用例为您提供了一些如何在协程中对其进行测试的见识] >>
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") }
CoroutineUtilKtTest.kt
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) } } }
LiveDataTestUtil.kt(这个想法来自aac sample,kotlin-coroutine)
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()
[email protected](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
}
class LiveDataValueCapture<T> {
val lock = Any()
private val _values = mutableListOf<T?>()
val values: List<T?>
get() = synchronized(lock) {
_values.toList() // copy to avoid returning reference to mutable list
}
fun addValue(value: T?) = synchronized(lock) {
_values += value
}
}
inline fun <T> LiveData<T>.captureValues(block: LiveDataValueCapture<T>.() -> Unit) {
val capture = LiveDataValueCapture<T>()
val observer = Observer<T> {
capture.addValue(it)
}
observeForever(observer)
try {
capture.block()
} finally {
removeObserver(observer)
}
}