Ali
Ali

Reputation: 9994

UniTest viewModel when using Deferred in Coroutines and Retrofit

I want to write a unitTest for my 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)
        }
    }
}

I receive following Exception :

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)

And here is my 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"))
        }
    }
}

When we use Rxjava we can create an Observable as simple as :

val observableResponse = Observable.just(SavingsGoalWrapper(listOf(savingsGoal)))
`when`(api.requestSavingGoals()).thenReturn(observableResponse)

How about Deferred in Coroutines? How can I test my method :

fun searchHero(@Path("name") name: String): Deferred<Response<HeroWrapper>>

Upvotes: 2

Views: 975

Answers (1)

Dan 0
Dan 0

Reputation: 924

The best way I've found to do this is to inject a CoroutineContextProvider and provide a TestCoroutineContext in test. My Provider interface looks like this:

interface CoroutineContextProvider {
    val io: CoroutineContext
    val ui: CoroutineContext
}

The actual implementation looks something like this:

class AppCoroutineContextProvider: CoroutineContextProvider {
    override val io = Dispatchers.IO
    override val ui = Dispatchers.Main
}

And a test implementation would look something like this:

class TestCoroutineContextProvider: CoroutineContextProvider {
    val testContext = TestCoroutineContext()
    override val io: CoroutineContext = testContext
    override val ui: CoroutineContext = testContext
}

So your SuperHeroRemoteDataSource becomes:

@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"))
        }
    }
}

When you inject the TestCoroutineContextProvider you can then call methods such as triggerActions() and advanceTimeBy(long, TimeUnit) on the testContext so your test would look something like:

@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
}

Note you should inject the coroutine context provider into your ViewModel as well. Also TestCoroutineContext() has an ObsoleteCoroutinesApi warning on it as it will be refactored as part of the structured concurrency update, but as of right now there is no change or new way of doing this, see this issue on GitHub for reference.

Upvotes: 1

Related Questions