首页 > 解决方案 > 在 Coroutines 和 Retrofit 中使用 Deferred 时的 UniTest viewModel

问题描述

我想为我的 viewModel 类写一个 unitTest :

@RunWith(MockitoJUnitRunner::class)
class MainViewModelTest {

    @get:Rule
    var rule: TestRule = InstantTaskExecutorRule()

    @Mock
    private lateinit var context: Application
    @Mock
    private lateinit var api: SuperHeroApi
    @Mock
    private lateinit var dao: HeroDao

    private lateinit var repository: SuperHeroRepository
    private lateinit var viewModel: MainViewModel

    private lateinit var heroes: List<Hero>

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        val localDataSource = SuperHeroLocalDataSource(dao)
        val remoteDataSource = SuperHeroRemoteDataSource(context, api)

        repository = SuperHeroRepository(localDataSource, remoteDataSource)
        viewModel = MainViewModel(repository)

        heroes = mutableListOf(
            Hero(
                1, "Batman",
                Powerstats("1", "2", "3", "4", "5"),
                Biography("Ali", "Tehran", "first"),
                Appearance("male", "Iranian", arrayOf("1.78cm"), arrayOf("84kg"), "black", "black"),
                Work("Android", "-"),
                Image("url")
            )
        )
    }

    @Test
    fun loadHeroes() = runBlocking {
        `when`(repository.getHeroes(anyString())).thenReturn(Result.Success(heroes))

        with(viewModel) {
            showHeroes(anyString())

            assertFalse(dataLoading.value!!)
            assertFalse(isLoadingError.value!!)
            assertTrue(errorMsg.value!!.isEmpty())

            assertFalse(getHeroes().isEmpty())
            assertTrue(getHeroes().size == 1)
        }
    }
}

我收到以下异常:

java.lang.NullPointerException
    at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes$2.invokeSuspend(SuperHeroRemoteDataSource.kt:25)
    at |b|b|b(Coroutine boundary.|b(|b)
    at com.sample.android.superhero.data.source.SuperHeroRepository.getHeroes(SuperHeroRepository.kt:21)
    at com.sample.android.superhero.MainViewModelTest$loadHeroes$1.invokeSuspend(MainViewModelTest.kt:68)
Caused by: java.lang.NullPointerException
    at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes$2.invokeSuspend(SuperHeroRemoteDataSource.kt:25)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
    at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)

这是我的 RemoteDataSource 类:

@Singleton
class SuperHeroRemoteDataSource @Inject constructor(
    private val context: Context,
    private val api: SuperHeroApi
) : SuperHeroDataSource {

    override suspend fun getHeroes(query: String): Result<List<Hero>> = withContext(Dispatchers.IO) {
        try {
            val response = api.searchHero(query).await()
            if (response.isSuccessful && response.body()?.response == "success") {
                Result.Success(response.body()?.wrapper!!)
            } else {
                Result.Error(DataSourceException(response.body()?.error))
            }
        } catch (e: SocketTimeoutException) {
            Result.Error(
                DataSourceException(context.getString(R.string.no_internet_connection))
            )
        } catch (e: IOException) {
            Result.Error(DataSourceException(e.message ?: "unknown error"))
        }
    }
}

当我们使用时,Rxjava我们可以创建一个 Observable,如下所示:

val observableResponse = Observable.just(SavingsGoalWrapper(listOf(savingsGoal)))
`when`(api.requestSavingGoals()).thenReturn(observableResponse)

在协程中怎么Deferred样?如何测试我的方法:

fun searchHero(@Path("name") name: String): Deferred<Response<HeroWrapper>>

标签: androidretrofit2kotlin-coroutines

解决方案


我发现做到这一点的最好方法是注入 aCoroutineContextProvider并提供 a TestCoroutineContextin test。我的 Provider 界面如下所示:

interface CoroutineContextProvider {
    val io: CoroutineContext
    val ui: CoroutineContext
}

实际的实现看起来像这样:

class AppCoroutineContextProvider: CoroutineContextProvider {
    override val io = Dispatchers.IO
    override val ui = Dispatchers.Main
}

测试实现看起来像这样:

class TestCoroutineContextProvider: CoroutineContextProvider {
    val testContext = TestCoroutineContext()
    override val io: CoroutineContext = testContext
    override val ui: CoroutineContext = testContext
}

所以你的SuperHeroRemoteDataSource变成:

@Singleton
class SuperHeroRemoteDataSource @Inject constructor(
        private val coroutineContextProvider: CoroutineContextProvider,
        private val context: Context,
        private val api: SuperHeroApi
) : SuperHeroDataSource {

    override suspend fun getHeroes(query: String): Result<List<Hero>> = withContext(coroutineContextProvider.io) {
        try {
            val response = api.searchHero(query).await()
            if (response.isSuccessful && response.body()?.response == "success") {
                Result.Success(response.body()?.wrapper!!)
            } else {
                Result.Error(DataSourceException(response.body()?.error))
            }
        } catch (e: SocketTimeoutException) {
            Result.Error(
                    DataSourceException(context.getString(R.string.no_internet_connection))
            )
        } catch (e: IOException) {
            Result.Error(DataSourceException(e.message ?: "unknown error"))
        }
    }
}

当您注入时,TestCoroutineContextProvider您可以调用诸如triggerActions()advanceTimeBy(long, TimeUnit)之类的方法,testContext因此您的测试看起来像:

@Test
fun `test action`() {
    val repository = SuperHeroRemoteDataSource(testCoroutineContextProvider, context, api)

    runBlocking {
        when(repository.getHeroes(anyString())).thenReturn(Result.Success(heroes)) 
    }

    // NOTE: you should inject the coroutineContext into your ViewModel as well
    viewModel.getHeroes(anyString())

    testCoroutineContextProvider.testContext.triggerActions()

    // Do assertions etc
}

请注意,您也应该将协程上下文提供程序注入到您的 ViewModel 中。也TestCoroutineContext()有一个ObsoleteCoroutinesApi警告,因为它将作为结构化并发更新的一部分进行重构,但到目前为止,没有更改或新的方法,请参阅 GitHub 上的此问题以供参考


推荐阅读