#android #retrofit2 #kotlin-coroutines
#Android #модернизация 2 #kotlin-сопрограммы
Вопрос:
Я хочу написать unitTest для моего класса ViewModel :
@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 amp;amp; 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
, мы можем создать наблюдаемую так просто, как :
val observableResponse = Observable.just(SavingsGoalWrapper(listOf(savingsGoal)))
`when`(api.requestSavingGoals()).thenReturn(observableResponse)
Как насчет Deferred
сопрограмм? Как я могу протестировать свой метод :
fun searchHero(@Path("name") name: String): Deferred<Response<HeroWrapper>>
Ответ №1:
Лучший способ, который я нашел для этого, — ввести a CoroutineContextProvider
и предоставить a 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 amp;amp; 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
на нем есть предупреждение, поскольку оно будет переработано как часть обновления structured concurrency, но на данный момент нет никаких изменений или нового способа сделать это, см. Этот выпуск на GitHub для справки.
Комментарии:
1. 1 за вашу помощь до сих пор.
repository.getHeroes(anyString()
является функцией приостановки. Поэтому его следует вызватьrunBlocking
. Когда я запускаю тест, он никогда не заканчивается. Вы знаете, почему?2. Он застревает в этой строке :
when(repository.getHeroes("Batman")).thenReturn(Result.Success(heroes))
3. Ах да, к сожалению, вам все равно придется обернуть свои
when
verify
блоки andrunBlocking
.4. Но снова он застревает в следующей строке. Я вставил код в локальную ветку модульного тестирования в github, которым я поделился в вопросе. Вы знаете, как это решить?
5. Я вытащил и посмотрел на него. Произошло несколько вещей, которых я раньше не видел. Прежде всего,
SuperHeroRepository
это не макет, поэтому`when`
он блокируется, поскольку он пытается запустить фактический вызов. Самый чистый способ издеватьсяSuperHeroRepository
— сделать его интерфейсом. Кроме того,triggerActions
должно выполняться только послеviewModel.showHeroes
вызова.