在协程和改造中使用Deferred时的UniTest viewModel

时间:2019-04-20 19:30:39

标签: android retrofit2 kotlin-coroutines

我在Github中有以下项目:https://github.com/Ali-Rezaei/SuperHero-Coroutines

我想为我的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>>

1 个答案:

答案 0 :(得分:1)

我发现最好的方法是注入CoroutineContextProvider并在测试中提供TestCoroutineContext。我的提供程序界面如下:

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警告,因为它将在结构化并发更新中进行重构,但是截至目前,see this issue on GitHub for reference尚无更改或新方法