Hello I have a unit test which was using Mockito, I converted most part of it to use Mockk except one part where I want to create a Observer of android lifecycle
Mockito version which works
#Mock
private lateinit var dataObserver: Observer<Result<List<Character>>>
Mockk version which does not work
private var dataObserver: Observer<Result<List<Character>>> = mockk(relaxed = true)
My test case fails with the following error when I use Mockk for the above and passes when I use Mockito version
error message
java.lang.AssertionError: Verification failed: call 1 of 1: Observer(#2).onChanged(eq(Success([Character(name=myName, img=image, occupation=[], status=status, nickname=nickName, appearance=[])])))) was not called
at io.mockk.impl.recording.states.VerifyingState.failIfNotPassed(VerifyingState.kt:66)
at io.mockk.impl.recording.states.VerifyingState.recordingDone(VerifyingState.kt:42)
at io.mockk.impl.recording.CommonCallRecorder.done(CommonCallRecorder.kt:47)
at io.mockk.impl.eval.RecordedBlockEvaluator.record(RecordedBlockEvaluator.kt:60)
at io.mockk.impl.eval.VerifyBlockEvaluator.verify(VerifyBlockEvaluator.kt:30)
at io.mockk.MockKDsl.internalCoVerify(API.kt:143)
at io.mockk.MockKKt.coVerify(MockK.kt:175)
at io.mockk.MockKKt.coVerify$default(MockK.kt:172)
at com.example.breakingbad.MainActivityViewModelTest$fetchCharacters$1.invokeSuspend(MainActivityViewModelTest.kt:76)
Full test case
package com.example.breakingbad
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import com.example.breakingbad.data.DataRepository
import com.example.breakingbad.model.Character
import com.example.breakingbad.viewModel.MainActivityViewModel
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.*
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
//#RunWith(MockitoJUnitRunner::class)
#ExperimentalCoroutinesApi
class MainActivityViewModelTest {
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
private val testDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testDispatcher)
private val dataRepository: DataRepository = mockk(relaxed = true)
private val mainActivityViewModel = MainActivityViewModel(dataRepository)
// #Mock
// private lateinit var dataObserver: Observer<Result<List<Character>>>
private var dataObserver: Observer<Result<List<Character>>> = mockk(relaxed = true)
#Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
#After
fun cleanup() {
Dispatchers.resetMain()
testCoroutineScope.cleanupTestCoroutines()
}
#Test
fun fetchCharacters() {
testCoroutineScope.launch {
coEvery { dataRepository.getCharacters() } returns Result.success(arrayListOf(Character(
name = "myName",
img = "image",
occupation = arrayListOf(),
status = "status",
nickname = "nickName",
appearance = arrayListOf()
)))
mainActivityViewModel.fetchCharacters()
coVerify { dataRepository.getCharacters() }
coVerify { dataObserver.onChanged(
Result.success(listOf(Character (
name = "myName",
img = "image",
occupation = arrayListOf(),
status = "status",
nickname = "nickName",
appearance = arrayListOf()
)))
) }
mainActivityViewModel.charactersLiveData.removeObserver(dataObserver)
}
}
}
how to make the test case pass using Mockk? what am I doing wrong?
Edit
ViewModel
class MainActivityViewModel #Inject constructor(
private val dataRepository: DataRepository
): ViewModel() {
private val _charactersLiveData = MutableLiveData<Result<ArrayList<Character>>>()
val charactersLiveData: LiveData<Result<ArrayList<Character>>> = _charactersLiveData
fun fetchCharacters() {
viewModelScope.launch(Dispatchers.IO) {
_charactersLiveData.postValue(dataRepository.getCharacters())
}
}
}
An a form to test a liveData is capturing the data passing through by the observer.
PD: In your case maybe he forgot add observerForever to livedata.
Anyway I leave an example:
private val dataObserver: Observer<Result<List<Character>>> = mockk()
private val argumentCaptor: CapturingSlot<Result<List<Character>> = slot()
#Test
fun fetchCharacters() = runBlockingTest {
//Move to #Before
mainActivityViewModel.charactersLiveData.observerForever(dataObserver)
coEvery { dataRepository.getCharacters() } returns Result.success(arrayListOf(Character(
name = "myName",
img = "image",
occupation = arrayListOf(),
status = "status",
nickname = "nickName",
appearance = arrayListOf()
)))
mainActivityViewModel.fetchCharacters()
coVerify { dataRepository.getCharacters() }
verify { dataObserver.onChanged(capture(argumentCaptor)) }
with(argumentCaptor){
//Here can be assert data, example:
assert(this is Result.Success)
assertEquals(YourData, this.data) //validation according to your data structure
}
//Move to #After
mainActivityViewModel.charactersLiveData.removeObserver(dataObserver)
}
Related
I am getting this exception while unit testing the viewModel.
Exception in thread "UI thread #coroutine#1" java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.paging.CachedPagingDataKt.cachedIn, parameter <this>
at androidx.paging.CachedPagingDataKt.cachedIn(CachedPagingData.kt)
at com.sarmad.newsprism.news.ui.NewsViewModel$getNewsStream$1.invokeSuspend(NewsViewModel.kt:46)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:829)
Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [CoroutineId(1), "coroutine#1":StandaloneCoroutine{Cancelling}#67526363, Dispatchers.Main]
expected:<false> but was:<true>
Expected :false
Actual :true
I want to test if newsViewModel.getNewsStream() is called, it should start loading, stop loading and expose updated UiState to NewsFragment so fragment can call adapter.submitData(data). But, there is an exception indication an error (I am a beginner and I can't understand that even after research for a good amount of time) in cachedIn(viewModelScope) operator while I am collecting flow in viewModel.
NewsViewModel
package com.sarmad.newsprism.news.ui
#HiltViewModel
class NewsViewModel #Inject constructor(
private val newsRepository: NewsRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
companion object {
const val KEY_SUBREDDIT = "us"
const val DEFAULT_SUBREDDIT = "androiddev"
}
init {
if (!savedStateHandle.contains(KEY_SUBREDDIT)) {
savedStateHandle[KEY_SUBREDDIT] = DEFAULT_SUBREDDIT
}
}
#OptIn(ExperimentalCoroutinesApi::class)
val articles = savedStateHandle.getLiveData<String>(KEY_SUBREDDIT)
.asFlow()
.flatMapLatest {
newsRepository.getBreakingNewsStream(it)
}.cachedIn(viewModelScope)
private val _userMessage = MutableStateFlow<String?>(null)
private val _isLoading = MutableStateFlow(false)
private val _newsArticles = articles
val uiState: StateFlow<NewsItemListUiState> = combine(
_isLoading, _userMessage, _newsArticles
) { isLoading, userMessage, newsArticles ->
NewsItemListUiState(
news = newsArticles,
isLoading = isLoading,
userMessage = userMessage
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = NewsItemListUiState(isLoading = true)
)
fun refresh() {
_isLoading.value = true
viewModelScope.launch {
newsRepository.refreshTasks()
_isLoading.value = false
}
}
}
NewsRepository
package com.sarmad.newsprism.data.repository
import androidx.paging.PagingData
import com.sarmad.newsprism.data.entities.NewsResponse
import kotlinx.coroutines.flow.Flow
import com.sarmad.newsprism.data.Result
import com.sarmad.newsprism.data.entities.Article
interface NewsRepository {
suspend fun getSearchedNewsStream(searchQuery: String, pageNumber: Int):
Flow<NewsResponse>
suspend fun getBreakingNewsStream(countryCode: String): Flow<PagingData<Article>>
}
NewsRepositoryImpl
package com.sarmad.newsprism.data.repository
import android.util.Log
import androidx.paging.*
import com.sarmad.newsprism.data.Result
import com.sarmad.newsprism.data.entities.Article
import com.sarmad.newsprism.data.entities.NewsResponse
import com.sarmad.newsprism.data.localdatasource.ArticleDao
import com.sarmad.newsprism.data.localdatasource.ArticleDatabase
import com.sarmad.newsprism.data.localdatasource.RemoteKeysDao
import com.sarmad.newsprism.data.paging.mediaters.NewsRemoteMediator
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.utils.Constants.Companion.PAGING_CONFIG_PAGE_SIZE
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.mapLatest
import javax.inject.Inject
private const val TAG = "NewsRepositoryImpl"
class NewsRepositoryImpl #Inject constructor(
private val api: NewsApi,
private val articleDao: ArticleDao,
private val articleDatabase: ArticleDatabase,
private val remoteKeysDao: RemoteKeysDao,
) : NewsRepository {
override suspend fun getSearchedNewsStream(
searchQuery: String,
pageNumber: Int
): Flow<NewsResponse> = flow {
val searchedNewsResponse = api.searchNews(searchQuery, pageNumber)
if (searchedNewsResponse.isSuccessful) searchedNewsResponse.body()
?.let { newsList -> emit(newsList) }
else emptyFlow<NewsResponse>()
}
#OptIn(ExperimentalPagingApi::class)
override suspend fun getBreakingNewsStream(
countryCode: String
): Flow<PagingData<Article>> {
return Pager(
config = PagingConfig(
pageSize = PAGING_CONFIG_PAGE_SIZE
),
remoteMediator = NewsRemoteMediator(articleDatabase, articleDao, remoteKeysDao, api),
pagingSourceFactory = { articleDao.getNewsStream() }
).flow
}
}
NewsRemoteMadiator
package com.sarmad.newsprism.data.paging.mediaters
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.data.entities.Article
import com.sarmad.newsprism.data.entities.ArticleRemoteKey
import com.sarmad.newsprism.data.localdatasource.ArticleDao
import com.sarmad.newsprism.data.localdatasource.ArticleDatabase
import com.sarmad.newsprism.data.localdatasource.RemoteKeysDao
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.math.ceil
#OptIn(ExperimentalPagingApi::class)
class NewsRemoteMediator #Inject constructor(
private val articleDatabase: ArticleDatabase,
private val articleDao: ArticleDao,
private val remoteKeysDao: RemoteKeysDao,
private val api: NewsApi
) : RemoteMediator<Int, Article>() {
override suspend fun initialize(): InitializeAction {
val newsCacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
val isSkipRefresh = remoteKeysDao.getLastUpdateTime()?.let {
System.currentTimeMillis() - it >= newsCacheTimeout
}
return if (isSkipRefresh == true) {
InitializeAction.SKIP_INITIAL_REFRESH
} else {
InitializeAction.LAUNCH_INITIAL_REFRESH
}
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, Article>
): MediatorResult {
return try {
val currentPage = when (loadType) {
LoadType.REFRESH -> {
val remoteKey = getRemoteKeyClosestToCurrentPosition(state)
remoteKey?.nextPage?.minus(1) ?: 1
}
LoadType.PREPEND -> {
val remoteKey = getRemoteKeyForFirstItem(state)
val prevPage = remoteKey?.prevPage ?: return MediatorResult.Success(
remoteKey != null
)
prevPage
}
LoadType.APPEND -> {
val remoteKey = getRemoteKeyForLastItem(state)
val nextPage =
remoteKey?.nextPage
?: return MediatorResult.Success(remoteKey != null)
nextPage
}
}
val response = api.getBreakingNews("us", currentPage)
val totalPages = response.body()?.totalResults?.toDouble()?.div(20)?.let { pages ->
ceil(pages)
}?.toInt()
val endOfPaginationReached = totalPages == currentPage
val nextPage = if (endOfPaginationReached) null else currentPage.plus(1)
val prevPage = if (currentPage == 1) null else currentPage.minus(1)
articleDatabase.withTransaction {
if (loadType == LoadType.REFRESH) {
articleDao.deleteAllArticles()
remoteKeysDao.deleteAllArticleRemoteKeys()
}
response.body()?.let { response ->
val keys = articleDao.insertAll(response.articles)
val mappedKeysToArticles = keys.map { key ->
ArticleRemoteKey(
id = key.toInt(),
nextPage = nextPage,
prevPage = prevPage,
modifiedAt = System.currentTimeMillis()
)
}
remoteKeysDao.insertArticleRemoteKeys(mappedKeysToArticles)
}
}
MediatorResult.Success(endOfPaginationReached)
} catch (ex: java.lang.Exception) {
return MediatorResult.Error(ex)
}
}
private suspend fun getRemoteKeyClosestToCurrentPosition(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.anchorPosition?.let { pos ->
state.closestItemToPosition(pos)?.id?.let { id ->
remoteKeysDao.getArticleRemoteKey(id)
}
}
}
private suspend fun getRemoteKeyForFirstItem(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.pages.firstOrNull {
it.data.isNotEmpty()
}?.data?.firstOrNull().let {
it?.let { it1 -> remoteKeysDao.getArticleRemoteKey(it1.id) }
}
}
private suspend fun getRemoteKeyForLastItem(
state: PagingState<Int, Article>
): ArticleRemoteKey? {
return state.pages.lastOrNull {
it.data.isNotEmpty()
}?.data?.lastOrNull().let {
it?.let { it1 -> remoteKeysDao.getArticleRemoteKey(it1.id) }
}
}
}
NewsViewModelTest
package com.sarmad.newsprism.news.ui
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.sarmad.newsprism.data.remotedatasource.api.NewsApi
import com.sarmad.newsprism.data.repository.NewsRepository
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
internal class NewsViewModelTest {
#Mock
lateinit var newsRepository: NewsRepository
#Mock
private lateinit var newsApi: NewsApi
private lateinit var newsViewModel: NewsViewModel
#OptIn(DelicateCoroutinesApi::class)
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
#OptIn(ExperimentalCoroutinesApi::class)
#Before
fun setUp() {
MockitoAnnotations.openMocks(this)
Dispatchers.setMain(mainThreadSurrogate)
newsViewModel = NewsViewModel(newsRepository)
}
#OptIn(ExperimentalCoroutinesApi::class)
#After
fun tearDown() {
Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher
mainThreadSurrogate.close()
}
#OptIn(ExperimentalCoroutinesApi::class)
#Test
fun test_getNewsFlow() = runTest {
newsViewModel.getNewsStream("us")
assertEquals(true, newsViewModel.newsFlow.value.isLoading)
advanceUntilIdle()
assertEquals(
false,
newsViewModel.newsFlow.value.isLoading
)
assertNotNull(
newsViewModel.newsFlow.value.news
)
}
}
The best way to check a call is using a Mock.
Create an interface for your NewsViewModel like INewsViewModel and inject it by using the constructor or the Setup. Depending on your Mock package it can be created like:
//Implementation using Moq
Mock<INewsViewModel> mock = new Mock<INewsViewModel>();
mock.Setup(m => m.getNewsStream());
// Your test
mock.VerifyAll();
Moq also allows to create a Mock when the mocked class has an empty constructor.
How to fix IllegalStateException: Attempt to collect twice from pageEventFlow, which is an illegal operation.
Did you forget to call Flow<PagingData<*>>.cachedIn(coroutineScope)?
Code:
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.switchMap
import androidx.lifecycle.viewModelScope
import androidx.paging.cachedIn
import com.kharismarizqii.movieapp.data.MovieRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
#HiltViewModel
class MovieViewModel #Inject constructor(
private val repository: MovieRepository,
state: SavedStateHandle) : ViewModel(){
companion object{
private const val CURRENT_QUERY = "current_query"
private const val EMPTY_QUERY = ""
}
private val currentQuery = state.getLiveData(CURRENT_QUERY, EMPTY_QUERY)
val movies = currentQuery.switchMap { query ->
if (query.isNotEmpty()){
repository.getSearchMovies(query)
}else{
repository.getNowPlayingMovies().cachedIn(viewModelScope)
}
}
fun searchMovies(query: String){
currentQuery.value = query
}
}
Crash:
java.lang.IllegalStateException: Attempt to collect twice from pageEventFlow, which is an illegal operation. Did you forget to call Flow<PagingData<*>>.cachedIn(coroutineScope)?
You should cache the data for getSearchMovies too
private val currentQuery = state.getLiveData(CURRENT_QUERY, EMPTY_QUERY)
val movies = currentQuery.switchMap { query ->
if (query.isNotEmpty()){
repository.getSearchMovies(query).cachedIn(viewModelScope)
}else{
repository.getNowPlayingMovies().cachedIn(viewModelScope)
}
}
I have made a simple unit test which tests a coroutines function which uses firebase.
I've mocked all the dependencies and the methods being used in this function I'm testing, but it continues to hang. I'm not sure exactly where it's hanging, but I would assume on the mocking of firestore, and it has await().
Test Class:
import android.content.Context
import com.example.socialtoker.data.db.UserDao
import com.example.socialtoker.data.repository.UserDataRepository
import com.example.socialtoker.data.repository.UserDataRepositoryImpl
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.database.DatabaseReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.storage.FirebaseStorage
import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
#RunWith(JUnit4::class)
#ExperimentalCoroutinesApi
class UserDataRepositoryImplTest {
private val contextMock = mockk<Context>{
every { getExternalFilesDir(any())?.absolutePath } returns "src/SocialToker/"
}
private val firestoreMock = mockk<FirebaseFirestore>{
coEvery { collection("Users").document(any()).set(any()).await() } returns mockk()
}
private val firebaseAuthMock = mockk<FirebaseAuth>{
coEvery { createUserWithEmailAndPassword(any(), any()) } returns mockk()
every { currentUser?.uid } returns "UID"
}
private val firebaseStorageMock = mockk<FirebaseStorage>()
private val firebaseDatabaseMock = mockk<DatabaseReference>()
private val daoMock = mockk<UserDao>{
coEvery { addUser(any()) } returns mockk()
}
private lateinit var userDateRepository: UserDataRepository
private val emailAddress = "socialtoker#socialtoker.com"
private val password = "socialtokerpassword"
private val username = "socialtoker"
#Before
fun setup() {
userDateRepository = UserDataRepositoryImpl(
contextMock,
firestoreMock,
firebaseAuthMock,
firebaseStorageMock,
firebaseDatabaseMock,
daoMock
)
}
#Test
fun `createUser calls firebase and stores user info locally and remotely`() = runBlocking {
// WHEN
userDateRepository.createUser(emailAddress, password, username)
//THEN
coVerify { firebaseAuthMock.createUserWithEmailAndPassword(emailAddress, password) }
}
}
Test Subject:
override suspend fun createUser(email: String, password: String, username: String): AuthResult {
try {
val data = hashMapOf(
"name" to username
)
val authResult = firebaseAuth.createUserWithEmailAndPassword(email, password).await()
val uid = firebaseAuth.currentUser!!.uid
userDao.addUser(UserData(uid, username, "", ""))
firestoreRef.collection("Users")
.document(uid)
.set(data).await()
return authResult
} catch (error: Throwable) {
throw RepositoryError(
error.localizedMessage ?: "Unable to create user", error
)
}
}
Please note that await is an extension function on Task class.
Therefore Mocking extension functions might need to be taken into consideration.
I know this has been asked a number of different ways with a number of different answers but none I can see help my situation. I am trying to test a presenter using coroutines. In order to verify behaviour I have implemented com.nhaarman.mockitokotlin2.mock. All tests pass individually but when run together they fail randomly (2 pass, 1 pass, 3 pass etc.)
I don't have that much experience with coroutines so I'm hoping someone with a bit more experience could steer me here. Below is my test class. I can add more info if required
package com.project.ui.search.results
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.whenever
import ie.distilledsch.dschapi.models.search.SavedSearchCreateResponse
import io.reactivex.schedulers.TestScheduler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.doThrow
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import retrofit2.HttpException
#ExperimentalCoroutinesApi
class SPSearchResultsPresenterSavedSearchesTest {
#get:Rule val testCoroutineRule = TestCoroutineRule()
private lateinit var interactor: SPSearchResultsInteractor
#Mock private lateinit var view: SPSearchResultsView
#Mock private lateinit var context: Context
#Mock private lateinit var interactorBtwAdListAndAdDetails: InteractorBetweenListOfAdsAndAdDetails
#Mock private lateinit var daftLoginManager: DaftLoginManager
private lateinit var searchForm: SearchForm
private lateinit var searchManager: SearchManager
private lateinit var testScheduler: TestScheduler
lateinit var presenter: SPSearchResultsPresenter
#Before
fun setup() {
MockitoAnnotations.initMocks(this)
interactor = mock()
testScheduler = TestScheduler()
searchForm = SearchForm()
searchManager = SearchManager(searchForm)
val presenterToTest = SPSearchResultsPresenterImpl(testScheduler, testScheduler,
testScheduler, Dispatchers.Main, Dispatchers.IO, interactorBtwAdListAndAdDetails, daftLoginManager, context, view)
presenterToTest.interactor = interactor
presenterToTest.searchManager = searchManager
presenter = presenterToTest
}
#Test
fun `save search dialog confirm click success`() = runBlocking {
val savedSearchTitle = "title"
val savedSearchID = 1234
val responseModel = SavedSearchCreateResponse(201, "Created", savedSearchID)
whenever(interactor.saveNewSearch(savedSearchTitle)).thenReturn(responseModel)
presenter.onSaveSearchDialogOkClick(savedSearchTitle)
verify(interactor).saveNewSearch(savedSearchTitle)
verify(view).displayIconForSavedSearch()
}
#Test
fun `save search dialog confirm click failure`() = runBlocking {
val savedSearchTitle = "title"
val exception :HttpException = mock()
doThrow(exception).`when`(interactor).saveNewSearch(savedSearchTitle)
presenter.onSaveSearchDialogOkClick(savedSearchTitle)
verify(interactor).saveNewSearch(savedSearchTitle)
verify(view, never()).displayIconForSavedSearch()
verify(view).showFetchSavedSearchFailedToast()
}
#Test
fun `delete search success logged in`() = runBlocking {
val savedSearchID = 1234
whenever(interactor.isUserLoggedIn()).thenReturn(true)
searchManager.isSearchSaved = true
searchManager.savedSearchId = savedSearchID
presenter.onSaveMenuItemClick()
verify(interactor).deleteSavedSearch(savedSearchID)
verify(view).displayIconForUnsavedSearch()
}
#Test
fun `delete search error logged in`() = runBlocking {
val savedSearchID = 1234
val exception :HttpException = mock()
whenever(interactor.isUserLoggedIn()).thenReturn(true)
searchManager.isSearchSaved = true
searchManager.savedSearchId = savedSearchID
whenever(interactor.deleteSavedSearch(savedSearchID)).thenThrow(exception)
presenter.onSaveMenuItemClick()
verify(interactor).deleteSavedSearch(savedSearchID)
verify(view, never()).displayIconForUnsavedSearch()
verify(view).showSnackBar(ArgumentMatchers.anyInt())
}
#Test
fun `click on save search not logged in` () = runBlocking {
whenever(interactor.isUserLoggedIn()).thenReturn(false)
presenter.onSaveMenuItemClick()
verify(view).showLoginScreen()
}
}
It depends what is in your TestCoroutineRule class, but one thing you should definitely do is inject a TestDispatcher into your presenter, in place of both Dispatchers.Main and Dispatchers.IO.
I want to unit test a method in viewmodal but everytime i failed, and had gone through many of the websites and stack answers but none of them helped out.
I just wanted to test a method in my viewmodal that is loadWeatherForeCast
I had gone through the following links,
Error calling Dispatchers.setMain() in unit test
https://android.jlelse.eu/mastering-coroutines-android-unit-tests-8bc0d082bf15
https://proandroiddev.com/mocking-coroutines-7024073a8c09
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import go_jek.domain.entities.LocationTemperature
import go_jek.domain.interactor.Result
import go_jek.domain.interactor.weatherUseCase.WeatherParam
import go_jek.domain.interactor.weatherUseCase.WeatherUseCase
import go_jek.domain.repository.ApiDataSource
import go_jek.utility.dateUtils.DateUtils
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class WeatherViewModelTest {
#get:Rule
val rule = InstantTaskExecutorRule()
#MockK
lateinit var apiDataSource: ApiDataSource //Interface
#MockK
lateinit var dateUtilImpl: DateUtils //Interface
#MockK
lateinit var weatherParam: WeatherParam
#MockK
lateinit var locationTemperatureOutput: LocationTemperature
private lateinit var weatherUseCase: WeatherUseCase
private lateinit var weatherViewModel: WeatherViewModel
#BeforeAll
fun setup() {
MockKAnnotations.init(this)
weatherUseCase = WeatherUseCase(apiDataSource)
weatherViewModel = WeatherViewModel(weatherUseCase, dateUtilImpl, Dispatchers.Unconfined)
}
#Test
fun check() = runBlocking {
every {
weatherUseCase.execute(any(), any(), any())
} answers {
thirdArg<(Result<LocationTemperature>) -> Unit>().invoke(Result.Success(locationTemperatureOutput))
}
weatherViewModel.loadWeatherForeCast(32.45, 72.452)
verify(atLeast = 1) {
apiDataSource.getWeatherInfo(weatherParam)
}
}
}
interface ApiDataSource {
fun getWeatherInfo(weatherParam: WeatherParam): Result<LocationTemperature>
}
class WeatherUseCase(var apiDataSource: ApiDataSource) : UseCase<LocationTemperature, WeatherParam>() {
override suspend fun run(params: WeatherParam): Result<LocationTemperature> = apiDataSource.getWeatherInfo(params)
}
class WeatherParam(
val apiKey: String = BuildConfig.appixu_secretkey,
val location: String
) : UseCase.NoParams()
class LocationTemperature(val location: Location, val current: Current, val forecast: Forecast) : Parcelable
class WeatherViewModel (val weatherUseCase: WeatherUseCase,
val dateUtilImpl: DateUtils,
val uiContext: CoroutineContext = Dispatchers.Main) : ViewModel(), CoroutineScope {
private val job: Job
private val _locationLiveData = MutableLiveData<LocationTemperature>()
val locationLiveData: LiveData<LocationTemperature>
private val _error: MutableLiveData<String> = MutableLiveData()
var error: LiveData<String> = _error
private val loadingState = MutableLiveData<Boolean>()
val loadingLiveData = loadingState
override val coroutineContext: CoroutineContext
get() = uiContext + job
init {
job = Job()
locationLiveData = Transformations.map(_locationLiveData) {
it.apply {
forecast.forecastday.forEach {
it.dayOfWeek = dateUtilImpl.format(it.date, DateUtils.FOR_YYYY_H_MM_H_DD, FULL_DAY)
}
}
}
}
fun handleError(error: Exception) {
loadingState.value = false
_error.value = error.localizedMessage
}
fun loadWeatherForeCast(latitude: Double, longitude: Double) {
val weatherParam = WeatherParam(location = String.format(Locale.getDefault(), "%1f, %2f",
latitude, longitude))
weatherUseCase.execute(this, weatherParam)
{
when (it) {
is Result.Success -> {
loadingState.value = false
_locationLiveData.value = it.data
}
is Result.Error -> {
handleError(it.exception)
}
}
}
}
override fun onCleared() {
job.cancel()
super.onCleared()
}
}
To use mockK with coroutines you need to use the new "coEvery" and "coVerify" functions and it will work better :
https://mockk.io/#coroutines