I have the following classes
interface CarsApi {
suspend fun fetchCar() : Car
}
class FetchCarUseCase(private val carsApi: CarsApi) {
suspend fun execute: Car = withContext(dispatcherProvider.io()) {
carsApi.fetchCar()
}
}
class ViewModel(private val fetchCarUseCase: FetchCarUseCase) {
private var car: Car
suspend fun retrieveCar() {
car = fetchCarUseCase.execute()
}
}
I want to write an ermetic test for the viewModel and the useCase:
#Test
fun testCarFetching() = runBlockingTest {
val aCar = Car()
val mockApi = mock<CarsApi>()
`when`(mockApi.fetchCar()).thenReturn(aCar)
val fetchCarUseCase = FetchCarUseCase(mockApi)
val viewModel = ViewModel(fetchCarUseCase)
viewModel.retrieveCar()
/* assert stuff on viewModel.car*/
}
But the viewModel.car always seems to be null. Inside the test body mockApi.fetchCar() does retrieve the provided value, but inside the FetchCarUseCase it does not. Also if I remove the suspend keyword from the interface, the mocking seems to be working fine.
At the moment, due to some other conditions I cannot use Mockk library, so I'm stuck with Mockito.
Am I missing something?
The used dependencies:
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.28.2'
testImplementation('com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0') {
exclude module: 'mockito-core'
}
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2
In case anyone else has to deal with this problem, here is the infrastructure I have build.
First, in all the classes that launch threads inject through the constructor or property a kotlinx.coroutines.DispatcherProvider. In my case it was just the useCase, but the viewModel might require it, as well.
class FetchCarUseCase(private val dispatcher: CoroutineDispatcher,
private val carsApi: CarsApi) {
suspend fun execute: Car = withContext(dispatcher) {
carsApi.fetchCar()
}
}
In the unit tests project, add a helper rule-class, in order to extract some functionality:
#ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {
val testDispatcherProvider = object : DispatcherProvider {
override fun default(): CoroutineDispatcher = testDispatcher
override fun io(): CoroutineDispatcher = testDispatcher
override fun main(): CoroutineDispatcher = testDispatcher
override fun unconfined(): CoroutineDispatcher = testDispatcher
}
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
And finally the unit test looks like this:
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class ViewModelTest {
#get:Rule
var coroutinesTestRule = CoroutineTestRule()
#Test
fun testCarFetching() = coroutinesTestRule.testDispatcher.runBlockingTest {
val aCar = Car()
val mockApi = mock<CarsApi>()
`when`(mockApi.fetchCar()).thenReturn(aCar)
val fetchCarUseCase = FetchCarUseCase(mockApi)
val viewModel = ViewModel(fetchCarUseCase)
viewModel.retrieveCar()
/* assert stuff on viewModel.car*/
}
#Test
fun testCarFetchingError() = coroutinesTestRule.testDispatcher.runBlockingTest {
val aCar = Car()
val mockApi = mock<CarsApi>()
`when`(mockApi.fetchCar()).then {
throw Exception()
}
val fetchCarUseCase = FetchCarUseCase(mockApi)
val viewModel = ViewModel(fetchCarUseCase)
viewModel.retrieveCar()
/* assert stuff on erros*/
}
}
This way all the code in the unit tests runs on the same thread and in the same context.
Related
I am unit testing the following class
class LoadTrendingSearchUseCaseImp #Inject constructor(
private val searchCriteriaProvider: SearchCriteriaProvider,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : LoadTrendingSearchUseCase {
override suspend fun execute(): List<String> {
return withContext(coroutineDispatcherProvider.io()) {
searchCriteriaProvider.provideTrendingSearch().trendingSearches
}
}
}
interface SearchCriteriaProvider {
suspend fun provideTrendingSearch(): CatalogSearchPage
}
class SearchCriteriaProviderImp() : SearchCritieraProvider {
override suspend fun provideTrendingSearch(): CatalogSearchPage {
return withContext(coroutineDispatcherProvider.io()) {
/* long running task */
}
}
}
interface CoroutineDispatcherProvider {
fun io(): CoroutineDispatcher = Dispatchers.IO
fun default(): CoroutineDispatcher = Dispatchers.Default
fun main(): CoroutineDispatcher = Dispatchers.Main
fun immediate(): CoroutineDispatcher = Dispatchers.Main.immediate
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}
class CoroutineDispatcherProviderImp #Inject constructor() : CoroutineDispatcherProvider
This is my actual test:
class LoadTrendingSearchUseCaseImpTest {
private val searchCriteriaProvider: SearchCriteriaProvider = mock()
private val coroutineDispatcherProvider = CoroutineDispatcherProviderImp()
private lateinit var loadTrendingSearchUseCaseImp: LoadTrendingSearchUseCaseImp
#Before
fun setUp() {
loadTrendingSearchUseCaseImp = LoadTrendingSearchUseCaseImp(
searchCriteriaProvider,
coroutineDispatcherProvider
)
}
#Test
fun `should provide trending searches`() {
runBlockingTest {
// Arrange
// EXCEPTION HERE whenever(searchCriteriaProvider.provideTrendingSearch().trendingSearches).thenReturn(
emptyList()
)
// Act
val actualResult = loadTrendingSearchUseCaseImp.execute()
// Assert
assertThat(actualResult).isEmpty()
}
}
}
The actual error message:
java.lang.NullPointerException
.product_search.usecase.imp.LoadTrendingSearchUseCaseImpTest$should provide trending searches$1.invokeSuspend(LoadTrendingSearchUseCaseImpTest.kt:30)
You tried to chain invocations when stubbing a method.
whenever(searchCriteriaProvider.provideTrendingSearch().trendingSearches)
.thenReturn(emptyList())
During stubbing, the actual methods are being called.
searchCriteriaProvider.provideTrendingSearch() returns null, as this call is not stubbed yet
subsequent call null.trendingSearches results in NPE
You need to stub each call in the chain
whenever(searchCriteriaProvider.provideTrendingSearch())
.thenReturn(catalogSearchPage)
whenever(catalogSearchPage.trendingSearches)
.thenReturn(emptyList())
Obviously, this assumes that
catalogSearchPage is also a mock
trendingSearches is a property
Alternatively, you can construct a POJO for catalogSearchPage, and return it in the first stubbing.
using mockk 1.9.3
having a function to be verified
class EventLogger private constructor()
fun logUserEvent(eventName: String?, eventParamMap: MutableMap<String, String>?) {
......
internaLogEventImpl(eventName, eventParamMap)
}
internal fun internaLogEventImpl(eventName: String?, customParams: MutableMap<String, String>?) {
......
}
companion object {
#Volatile
private var sEventLoggerSingleton: EventLogger? = null
#JvmStatic
val instance: EventLogger
get() {
if (sEventLoggerSingleton == null) {
sEventLoggerSingleton = EventLogger()
}
return sEventLoggerSingleton!!
}
}
got compiler error at every {eventLogger.internaLogEventImpl(any(), mapSlot)}
Type mismatch.
Required: MutableMap<String, String>?
Found: CapturingSlot<MutableMap<String, String>>
when trying this below :
class TestK {
lateinit var eventLogger: EventLogger
lateinit var application: Application
val mapSlot = slot<MutableMap<String, String>>()
#Before
fun setUp() {
application = ApplicationProvider.getApplicationContext<Application>()
eventLogger = mockk.spyk(EventLogger.instance)
ReflectionHelpers.setStaticField(EventLogger::class.java, "sEventLoggerSingleton", eventLogger)
}
#After
fun cleanUp() {
ReflectionHelpers.setStaticField(EventLogger::class.java, "sEventLoggerSingleton", null)
}
#Test
fun logNotificationStatusChange_with_enabled_WhenCalled_ShouldLog() {
val testMap = hashMapOf("action" to "open")
every {eventLogger.internaLogEventImpl(any(), mapSlot)} answers {
println(mapSlot.captured)
assert(mapSlot.captured["action"] == "open")
}
eventLogger.logUserEvent("test_event", testMap)
}
}
You need to use capture (see Mockk's Capturing section).
So for your case capture(mapSlot) should work.
eventLogger.internaLogEventImpl(any(), capture(mapSlot))
Before trying to mock on complex code, It would be better to learn on easier example.
Here is a working example to mock a private call on an object with mockk.
object MyLogger {
fun logUserEvent(event: String?, map: MutableMap<String, String>?) {
// turns the event string into an uppercase string.
internaLogEventImpl(event?.toUpperCase(), map)
}
private fun internaLogEventImpl(event: String?, map: MutableMap<String, String>?): Unit =
throw Exception("real implementation")
}
How to test and mock the internal function so we don't throw the exception.
#Test
fun `test logger internal`() {
val expectedMap = mutableMapOf("a" to "b")
val expectedEvent = "EVENT"
val mock = spyk(MyLogger, recordPrivateCalls = true)
justRun { mock["internaLogEventImpl"](expectedEvent, expectedMap) }
// or justRun { mock["internaLogEventImpl"](any<String>(), any<MutableMap<String, String>>()) }
mock.logUserEvent("event", expectedMap)
verify { mock["internaLogEventImpl"](expectedEvent, expectedMap) }
}
Here logUserEvent calls the real implementation and internaLogEventImpl calls the mock implementation.
if justRun { mock["internaLogEventImpl"](expectedEvent, expectedMap) } is not called (or wrong because the argument doesn't match), the real implementation will be call. Here it will throw Exception("real implementation").
Please try and modify values to check different behaviors.
would someone be able to show me how to make the getMovies function in this viewModel testable? I can't get the unit tests to await the coroutines properly..
(1) I'm pretty sure I have to create a test-CoroutineScope and a normal lifeCycle-CoroutineScope, as seen in this Medium Article.
(2) Once the scope definitions are made, I'm also unsure how to tell getMovies() which scope it should be using given a normal app context or a test context.
enum class MovieApiStatus { LOADING, ERROR, DONE }
class MovieListViewModel : ViewModel() {
var pageCount = 1
private val _status = MutableLiveData<MovieApiStatus>()
val status: LiveData<MovieApiStatus>
get() = _status
private val _movieList = MutableLiveData<List<Movie>>()
val movieList: LiveData<List<Movie>>
get() = _movieList
// allows easy update of the value of the MutableLiveData
private var viewModelJob = Job()
// the Coroutine runs using the Main (UI) dispatcher
private val coroutineScope = CoroutineScope(
viewModelJob + Dispatchers.Main
)
init {
Log.d("list", "in init")
getMovies(pageCount)
}
fun getMovies(pageNumber: Int) {
coroutineScope.launch {
val getMoviesDeferred =
MovieApi.retrofitService.getMoviesAsync(page = pageNumber)
try {
_status.value = MovieApiStatus.LOADING
val responseObject = getMoviesDeferred.await()
_status.value = MovieApiStatus.DONE
............
} catch (e: Exception) {
_status.value = MovieApiStatus.ERROR
................
}
}
pageCount = pageNumber.inc()
}
...
}
it uses this API service...
package com.example.themovieapp.network
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.Deferred
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
private const val BASE_URL = "https://api.themoviedb.org/3/"
private const val API_key = ""
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(BASE_URL)
.build()
interface MovieApiService{
//https://developers.themoviedb.org/3/movies/get-top-rated-movies
//https://square.github.io/retrofit/2.x/retrofit/index.html?retrofit2/http/Query.html
#GET("movie/top_rated")
fun getMoviesAsync(
#Query("api_key") apiKey: String = API_key,
#Query("language") language: String = "en-US",
#Query("page") page: Int
): Deferred<ResponseObject>
}
/*
Because this call is expensive, and the app only needs
one Retrofit service instance, you expose the service to the rest of the app using
a public object called MovieApi, and lazily initialize the Retrofit service there
*/
object MovieApi {
val retrofitService: MovieApiService by lazy {
retrofit.create(MovieApiService::class.java)
}
}
I'm simply trying to create a test which asserts the liveData 'status' is DONE after the function.
Here is the Project Repository
First you need to make your coroutine scope injectable somehow, either by creating a provider for it manually, or using an injection framework like dagger. That way, when you test your ViewModel, you can override the coroutine scope with a test version.
There are a few choices to do this, you can simply make the ViewModel itself injectable (article on that here: https://medium.com/chili-labs/android-viewmodel-injection-with-dagger-f0061d3402ff)
Or you can manually create a ViewModel provider and use that where ever it's created. No matter what, I would strongly advise some form of dependency injection in order to achieve real testability.
Regardless, your ViewModel needs to have its CoroutineScope provided, not instantiate the coroutine scope itself.
In other words you might want
class MovieListViewModel(val couroutineScope: YourCoroutineScope) : ViewModel() {}
or maybe
class MovieListViewModel #Inject constructor(val coroutineScope: YourCoroutineScope) : ViewModel() {}
No matter what you do for injection, the next step is to create your own CoroutineScope interface that you can override in the test context. For example:
interface YourCoroutineScope : CoroutineScope {
fun launch(block: suspend CoroutineScope.() -> Unit): Job
}
That way when you use the scope for your app, you can use one scope, say, lifecycle coroutine scope:
class LifecycleManagedCoroutineScope(
private val lifecycleCoroutineScope: LifecycleCoroutineScope,
override val coroutineContext: CoroutineContext = lifecycleCoroutineScope.coroutineContext) : YourCoroutineScope {
override fun launch(block: suspend CoroutineScope.() -> Unit): Job = lifecycleCoroutineScope.launchWhenStarted(block)
}
And for your test, you can use a test scope:
class TestScope(override val coroutineContext: CoroutineContext) : YourCoroutineScope {
val scope = TestCoroutineScope(coroutineContext)
override fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return scope.launch {
block.invoke(this)
}
}
}
Now, since your ViewModel is using a scope of type YourCoroutineScope, and since, in the examples above, both the lifecycle and test version implement the YourCoroutineScope interface, you can use different versions of the scope in different situations, i.e. app vs test.
Ok, thanks to Dapp's answer, I was able to write some tests which seem to be awaiting the function Properly.
Here is a copy of what I did :)
enum class MovieApiStatus { LOADING, ERROR, DONE }
class MovieListViewModel(val coroutineScope: ManagedCoroutineScope) : ViewModel() {
//....creating vars, livedata etc.
init {
getMovies(pageCount)
}
fun getMovies(pageNumber: Int) =
coroutineScope.launch{
val getMoviesDeferred =
MovieApi.retrofitService.getMoviesAsync(page = pageNumber)
try {
_status.value = MovieApiStatus.LOADING
val responseObject = getMoviesDeferred.await()
_status.value = MovieApiStatus.DONE
if (_movieList.value == null) {
_movieList.value = ArrayList()
}
pageCount = pageNumber.inc()
_movieList.value = movieList.value!!.toList().plus(responseObject.results)
.sortedByDescending { it.vote_average }
} catch (e: Exception) {
_status.value = MovieApiStatus.ERROR
_movieList.value = ArrayList()
}
}
fun onLoadMoreMoviesClicked() =
getMovies(pageCount)
//...nav functions, clearing functions etc.
}
and here are the test cases
#ExperimentalCoroutinesApi
#RunWith(MockitoJUnitRunner::class)
class MovieListViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
private val managedCoroutineScope: ManagedCoroutineScope = TestScope(testDispatcher)
lateinit var viewModel: MovieListViewModel
#Before
fun setup() {
//resProvider.mockColors()
Dispatchers.setMain(testDispatcher)
viewModel = MovieListViewModel(managedCoroutineScope)
}
#After
fun tearDown() {
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
#ExperimentalCoroutinesApi
#Test
fun getMoviesTest() {
managedCoroutineScope.launch {
assertTrue(
"initial List, API status: ${viewModel.status.getOrAwaitValue()}",
viewModel.status.getOrAwaitValue() == MovieApiStatus.DONE
)
assertTrue(
"movieList has ${viewModel.movieList.value?.size}, != 20",
viewModel.movieList.value?.size == 20
)
assertTrue(
"pageCount = ${viewModel.pageCount}, != 2",
viewModel.pageCount == 2
)
viewModel.onLoadMoreMoviesClicked()
assertTrue(
"added to list, API status: ${viewModel.status.getOrAwaitValue()}",
viewModel.status.getOrAwaitValue() == MovieApiStatus.DONE
)
assertTrue(
"movieList has ${viewModel.movieList.value?.size}, != 40",
viewModel.movieList.value?.size == 40
)
}
}
}
It took some trial and error playing around with the Scopes.. runBlockingTest{} was causing an issue 'Exception: job() not completed'..
I also had to create a viewModel factory in order for the fragment to create the viewModel for when the app is running normally..
Project Repo
I am trying to mock a method (getQualityControlCheckDataForUplift(ArgumentMatchers.any(), ArgumentMatchers.anyFloat())) to return qcchecks which I have defined in my test case but I get an error.
Any suggestions on where I am doing it wrong please
org.mockito.exceptions.base.MockitoException: Unable to create mock instance of type 'QualityControlChecksDataProvider'
QualityControlChecksDataProvider
class QualityControlChecksDataProvider #Inject constructor(
private val offlineDataStorage: OfflineDataStorage,
private val app: App
) {
private val mapOfQCChecksForMilestone = LinkedHashMap<String, ArrayList<QualityControlCheck>?>()
fun getQualityControlCheckDataForUplift(qualityControlMilestone: QualityControlMilestone, uplift: Float):
ArrayList<QualityControlCheck>? {
val qcChecksForUplift: ArrayList<QualityControlCheck>? = ArrayList()
val qcChecksForMilestone = mapOfQCChecksForMilestone[qualityControlMilestone.milestoneText]
qcChecksForMilestone?.forEach {
if (it.uplift == uplift) qcChecksForUplift?.add(it)
}
return qcChecksForUplift
}
}
In my unit test this is how I am trying to mock
#Test
fun `upliftedVolumeUpdated abcd` () {
val qualityControlCheckDataProvider = spy(QualityControlChecksDataProvider::class.java)
every(qualityControlCheckDataProvider.getQualityControlCheckDataForUplift(ArgumentMatchers.any(), ArgumentMatchers.anyFloat())).thenReturn(qualityControlChecks)
}
I don't think a spy is what you want here I think you want a mock. Give this a try
#Test
fun `upliftedVolumeUpdated abcd` () {
val qualityControlDataProviderMock = mock<QualityControlChecksDataProvider> {
on { getQualityControlCheckDataForUplift(any(), any()) } doReturn qualityControlChecks
}
}
I created an App using coroutines and retrofit and it works fine. The problem comes when I try to create UT for the Presenter. Here how I made the presenter:
class MainPresenter : ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
private lateinit var listener: ActivityStatesListener
fun setActivityListener(listener: ActivityStatesListener) {
this.listener = listener
}
fun getHeroesFromRepository(page: Int) {
GlobalScope.launch(Dispatchers.Main) {
try {
val response = heroesRepository.getHeroes(page)
listener.onHeroesReady(response.data.results)
} catch (e: HttpException) {
listener.onError(e.message())
} catch (e: Throwable) {
listener.onError(e.message)
}
}
}
override fun onCleared() {
super.onCleared()
compositeDisposable.dispose()
}
}
I started to make the UT for it and I made a small test but is giving me the following error: java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize
class HeroesDataSourceTest {
val heroesRepository: HeroesRepository = mock(HeroesRepository::class.java)
#Mock
lateinit var activityListener: ActivityStatesListener
val hero = Heroes.Hero(1, "superman", "holasuperman", 1, null, null)
val results = Arrays.asList(hero)
val data = Heroes.Data(results)
val dataResult = Heroes.DataResult(data)
private val mainPresenter = MainPresenter()
#Before
fun initTest() {
MockitoAnnotations.initMocks(this)
}
#Test
fun testLoadInitialSuccess() = runBlocking(Dispatchers.Main) {
`when`(heroesRepository.getHeroes(0)).thenReturn(dataResult)
mainPresenter.getHeroesFromRepository(0)
verify(activityListener).onHeroesReady(dataResult.data.results)
}
}
Is clear that Dispatcher.Main is giving problems but I have no clue how to solve it.
EDIT
The repository used is the following:
class HeroesRepository {
val privateKey = "5009bb73066f50f127907511e70f691cd3f2bb2c"
val publicKey = "51ef4d355f513641b490a80d32503852"
val apiDataSource = DataModule.create()
val pageSize = 20
suspend fun getHeroes(page: Int): Heroes.DataResult {
val now = Date().time.toString()
val hash = generateHash(now + privateKey + publicKey)
val offset: Int = page * pageSize
return apiDataSource.getHeroes(now, publicKey, hash, offset, pageSize).await()
}
fun generateHash(variable: String): String {
val md = MessageDigest.getInstance("MD5")
val digested = md.digest(variable.toByteArray())
return digested.joinToString("") {
String.format("%02x", it)
}
}
}
I assume that heroesRepository.getHeroes(page) marked as suspend, so it will suspend the coroutine and not block the Main thread.
Try to follow the next approach:
// add `CoroutineContext` to the constructor to be replaceable from the tests
class MainPresenter(private val uiContext: CoroutineContext = Dispatchers.Main)
: ViewModel(), CoroutineScope {
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = uiContext + job
fun getHeroesFromRepository(page: Int) {
// use local scope to launch a coroutine
launch {
try {
val response = heroesRepository.getHeroes(page)
listener.onHeroesReady(response.data.results)
} catch (e: HttpException) {
listener.onError(e.message())
} catch (e: Throwable) {
listener.onError(e.message)
}
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
// ...
}
In the test class replace uiContext with another CoroutineContext:
class HeroesDataSourceTest {
// ... initializations
#Test
fun testLoadInitialSuccess() = runBlocking {
`when`(heroesRepository.getHeroes(0)).thenReturn(dataResult)
mainPresenter = MainPresenter(Dispatchers.Unconfined).apply {
getHeroesFromRepository(0)
}
// ... your tests here
}
}