Reputation: 9994
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
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