我在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>>
答案 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尚无更改或新方法