在协程和 Retrofit 中使用 Deferred 时的 UniTest viewModel
UniTest viewModel when using Deferred in Coroutines and Retrofit
我想为我的 viewModel 编写一个单元测试 class :
@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.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.invokeSuspend(MainViewModelTest.kt:68)
Caused by: java.lang.NullPointerException
at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes.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 class :
@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>>
我发现最好的方法是注入一个 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
时,您可以在 testContext
上调用 triggerActions()
和 advanceTimeBy(long, TimeUnit)
等方法,这样您的测试将类似于:
@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 .
我想为我的 viewModel 编写一个单元测试 class :
@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.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.invokeSuspend(MainViewModelTest.kt:68)
Caused by: java.lang.NullPointerException
at com.sample.android.superhero.data.source.remote.SuperHeroRemoteDataSource$getHeroes.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 class :
@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>>
我发现最好的方法是注入一个 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
时,您可以在 testContext
上调用 triggerActions()
和 advanceTimeBy(long, TimeUnit)
等方法,这样您的测试将类似于:
@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 .