UniTest viewModel при использовании Отложено в сопрограммах и модернизации - PullRequest
2 голосов
/ 20 апреля 2019

У меня есть следующий проект в Github: https://github.com/Ali-Rezaei/SuperHero-Coroutines

Я хочу написать 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 && 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 Ответ

1 голос
/ 20 апреля 2019

Лучший способ, который я нашел, - это ввести 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, поскольку оно будет реорганизовано как часть структурированного обновления параллелизма, но на данный момент нет изменений или нового способа сделать это, см. Эту проблему на GitHub для справка .

...