How to test suspend function using MockK? - android

I am writing a unit test for my Datarepository layer which simply calls an interface.
I am using Kotlin, coroutines and MockK for unit testing.
In MockK, how can I verify that I have called apiServiceInterface.getDataFromApi() and has happened only once?
Should I put the code in runBlocking?
This is my code:
UnitTest
import com.example.breakingbad.api.ApiServiceInterface
import com.example.breakingbad.data.DataRepository
import io.mockk.impl.annotations.InjectMockKs
import io.mockk.impl.annotations.MockK
import io.mockk.verify
import org.junit.Test
Repository
class DataRepositoryTest {
#MockK
private lateinit var apiServiceInterface: ApiServiceInterface
#InjectMockKs
private lateinit var dataRepository: DataRepository
#Test
fun getCharacters() {
val respose = dataRepository.getCharacters()
verify { apiServiceInterface.getDataFromApi() }
}
}
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCharacters(): Result<ArrayList<Character>> = kotlin.runCatching{
apiServiceInterface.getDataFromApi()
}
}
Interface
interface ApiServiceInterface {
#GET("api/characters")
suspend fun getDataFromApi(): ArrayList<Character>
}

I think you should prefer using runTest instead of runBlocking or runBlockingTest.To tell you in brief about the three.
runBlocking allows you to call suspend functions by blocking a new coroutine and it blocks the current thread until it is completed.
runBlockingTest will immediately execute the suspending function skipping past any delay and enter coroutine block immediately unlike runBlocking which will wait for the amount of the delay
Since kotlinx.coroutines 1.6.0 release, runBlockingTest is deprecated in favour of runTest due to these reasons listed in the migration guide.
runTest() will automatically skip calls to delay() and handle uncaught exceptions. Unlike runBlockingTest() , it will wait for asynchronous callbacks to handle situations where some code runs in dispatchers that are not integrated with the test module.
I hope that answers your question of what to choose among these 3 to test your suspending function. You code would look like this -:
#Test
fun getCharacters() = runTest {
val response = dataRepository.getCharacters()
coVerify { apiServiceInterface.getDataFromApi() }
}
Also note that as David mentioned above, because getDataFromApi() is asynchronous/suspending function as well, you will have to use coVerify instead of verify to mock the same.

Yes, you should put the dataRepository.getCharacters() call in a runBlocking.
And the verify should be replaced for coVerify.
In the end, the test should look like this:
#Test
fun getCharacters() {
val respose = runBlocking { dataRepository.getCharacters() }
coVerify { apiServiceInterface.getDataFromApi() }
}
Also, since you want to verify it has happened only once, you need to call coVerify with the exactly parameter coVerify(exactly = 1)

Related

Android ViewModel Coroutines launch Test not waiting

I'm trying to do ViewModel testing using Kotlin(1.6.21) Coroutines(1.6.4) and Kotlin Flow.
Following official Kotlin coroutine testing documentation but ViewModel is not waiting/returning a result for suspending functions before test completion. Have gone through top StackOverflow answers and tried all suggested solutions like injecting the same CoroutineDispatcher, and passing the same CoroutineScope but none worked so far. So here I am posting the current simple test implementation. Have to post all classes code involved in the test case to get a better idea.
ReferEarnDetailViewModel.kt:
Injected Usecase and CoroutineContextProvider and calling API using viewModelScope with provided dispatcher. But after calling callReferEarnDetails() from the test case, it is not collecting any data emitted by the mock use case method. Have tried with the direct repo method call, without Kotlin flow as well but same failure.
#HiltViewModel class
ReferEarnDetailViewModel #Inject constructor(
val appDatabase: AppDatabase?,
private val referEarnDetailsUseCase: ReferEarnDetailsUseCase,
private val coroutineContextProvider: CoroutineContextProvider) : BaseViewModel() {
fun callReferEarnDetails() {
setProgress(true)
viewModelScope.launch(coroutineContextProvider.default + handler) {
referEarnDetailsUseCase.execute(UrlUtils.getUrl(R.string.url_referral_detail))
.collect { referEarnDetail ->
parseReferEarnDetail(referEarnDetail)
}
}
}
private fun parseReferEarnDetail(referEarnDetail:
ResultState<CommonEntity.CommonResponse<ReferEarnDetailDomain>>) {
when (referEarnDetail) {
is ResultState.Success -> {
setProgress(false)
.....
}
}
}
ReferEarnCodeUseCase.kt: Returning Flow of Api response.
#ViewModelScoped
class ReferEarnCodeUseCase #Inject constructor(private val repository:
IReferEarnRepository) :BaseUseCase {
suspend fun execute(url: String):
Flow<ResultState<CommonEntity.CommonResponse<ReferralCodeDomain>>> {
return repository.getReferralCode(url)
}
}
CoroutineTestRule.kt
#ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestDispatcher =
StandardTestDispatcher()) : TestWatcher() {
val testCoroutineDispatcher = object : CoroutineContextProvider {
override val io: CoroutineDispatcher
get() = testDispatcher
override val default: CoroutineDispatcher
get() = testDispatcher
override val main: CoroutineDispatcher
get() = testDispatcher
}
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
}
}
ReferEarnDetailViewModelTest.kt
#RunWith(JUnit4::class)
#ExperimentalCoroutinesApi
class ReferEarnDetailViewModelTest {
private lateinit var referEarnDetailViewModel: ReferEarnDetailViewModel
private lateinit var referEarnDetailsUseCase: ReferEarnDetailsUseCase
#get:Rule
val coroutineTestRule = CoroutineTestRule()
#Mock
lateinit var referEarnRepository: IReferEarnRepository
#Mock
lateinit var appDatabase: AppDatabase
#Before
fun setUp() {
MockitoAnnotations.initMocks(this)
referEarnDetailsUseCase = ReferEarnDetailsUseCase(referEarnRepository)
referEarnDetailViewModel = ReferEarnDetailViewModel(appDatabase,
referEarnDetailsUseCase , coroutineTestRule.testCoroutineDispatcher)
}
#Test
fun `test api response parsing`() = runTest {
val data = ResultState.Success( TestResponse() )
//When
Mockito.`when`(referEarnDetailsUseCase.execute("")).thenReturn(flowOf(data))
//Call ViewModel function which further call usecase function.
referEarnDetailViewModel.callReferEarnDetails()
//This should be false after API success response but failing here....
assertEquals(referEarnDetailViewModel.showProgress.get(),false)
}
}
Have tried this solution:
How test a ViewModel function that launch a viewModelScope coroutine? Android
Kotlin
Inject and determine CoroutineScope on ViewModel creation
As it is stated in the documentation runTest awaits completion of all the launched in its TestScope coroutines (or throws a timeout). But it does so on exit from the test body. In your case assertEquals fails inside the test body, so test fails immediately.
Generally speaking, this mechanism of awaiting completion of all jobs is a mean of preventing leaks and is not suitable for your purpose.
There are two ways to control the coroutines execution inside the test body:
Use methods to control virtual time. E.g. advanceUntilIdle should help in this case - use it before asserting the result and it will execute all the tasks scheduled on the given TestDispatcher.
Use regular ways to await execution, e.g. return a job and await its' completion before checking the result. This requires some code redesign, but this is a recommended approach. Check out a couple of paragraphs above the Setting the Main dispatcher chapter.

What's different between these parameter vs injected Dispatchers?

These two methods invoke the same use-case. In the first version, I hard-coded Dispatchers.IO, and things work as expected.
The second version (which I prefer) uses an injected dispatcher that defaults to the Dispatchers.IO type. It fails with the IllegalStateException described in the comments. Any ideas?
#HiltViewModel
class MainViewModel #Inject constructor(
private val getUsers: GetUsers,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {
val liveData: MutableLiveData<List<User>> = MutableLiveData()
suspend fun getUsersByParamDispatcher(params: GetUsers.Params) {
// Successfully works as intended.
viewModelScope.launch(Dispatchers.IO) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
suspend fun getUsersByInjectDispatcher(params: GetUsers.Params) {
// IllegalStateException: Cannot access database on the main thread since it may potentially
// lock the UI for a long period of time.
// at androidx.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:494).
viewModelScope.launch(dispatcher) {
getUsers(params).collectLatest {
liveData.postValue(it)
}
}
}
}
Logs confirm the exception and my curiosity is why are they different and how would I arrive at a working injected version.
Failing injected Dispatchers.IO:
>> coroutine.name: main
Working parameter Dispatchers.IO:
>> coroutine.name: DefaultDispatcher-worker-1
The dependencies are provided by #HiltViewModel and I expect dispatcher to respect its assigned default value. The Fragment creates this view model with the by viewModels() delegate.
It might be fine to hard-code the dispatcher. But with injection a blocking TestCoroutineDispatcher is easily passed during testing.
Maybe I'm overlooking something simple, or another way altogether.
// MainViewModelTest
#Before
fun setup() {
MockKAnnotations.init(this)
viewModel = MainViewModel(
getUsers,
coroutinesTestRule.testDispatcher
)
}

What is the lifetime of coroutineScope in Kotlin?

The Code A is from the project architecture samples at https://github.com/android/architecture-samples
1: I don't know if the function activateTask(task: Task) need to be wrapped with runBlocking just like Code B. I'm afraid that activateTask(task: Task) maybe not be run if the object of DefaultTasksRepository is destroyed quickly.
2: Normally I run coroutines in ViewModel.viewModelScope, I don't know whether the ViewModel.viewModelScope will be destroyed when I finish the app, and whether the coroutines running in ViewModel.viewModelScope will be destroyed too. If so, I think it will be bad, some long time coroutines such as writing data to remote server will be cancel.
3: And more, the function activateTask in Code A is a coroutines function, it can invoke another coroutines function directly, so I think the Code A+ is correct, right?
Code A
import kotlinx.coroutines.coroutineScope
...
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {
...
override suspend fun activateTask(task: Task) = withContext<Unit>(ioDispatcher) {
coroutineScope {
launch { tasksRemoteDataSource.activateTask(task) }
launch { tasksLocalDataSource.activateTask(task) }
}
}
override suspend fun clearCompletedTasks() {
coroutineScope {
launch { tasksRemoteDataSource.clearCompletedTasks() }
launch { tasksLocalDataSource.clearCompletedTasks() }
}
}
...
}
Code A+
import kotlinx.coroutines.coroutineScope
...
class DefaultTasksRepository(
private val tasksRemoteDataSource: TasksDataSource,
private val tasksLocalDataSource: TasksDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksRepository {
...
override suspend fun activateTask(task: Task) = withContext<Unit>(ioDispatcher) {
tasksRemoteDataSource.activateTask(task)
tasksLocalDataSource.activateTask(task)
}
override suspend fun clearCompletedTasks() {
tasksRemoteDataSource.clearCompletedTasks()
tasksLocalDataSource.clearCompletedTasks()
}
...
}
Code B
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
You should not use runBlocking in any coroutine application, it blocks the thread.
If you really want to make activateTask non-cancellable there is a factory implementation of NonCancellable already in the stdlib
And you should not use coroutineScope wrapper inside the withContext, as a newly created CoroutineScope along with a new job is already passed as receiver within withContext.
Implement your activateTask like this:
override suspend fun activateTask(task: Task) = withContext<Unit>(ioDispatcher + NonCancellable) {
launch { tasksRemoteDataSource.activateTask(task) }
launch { tasksLocalDataSource.activateTask(task) }
}
In this way it will be called on the IODispatcher but will not be cancellable since the Job element of the resulting context does not provide functionality to cancel it.
ViewModelScope runs till your application is destroyed, more info and lifecycle chart is here. If you want to run some very important tasks, then use other dispatchers.
Yes code A+ is completely correct
PS: You should not implement runBlocking in a coroutine application, its default implementation is just the event loop.
runBlocking is the way to bridge synchronous and asynchronous code
Better implementation of main function should be:
suspend fun main() = coroutineScope {
// code here
}
It runs on the CommonPool, and if it suspends another coroutine could reuse the same thread.

Kotlin Coroutine Testing with Dispatchers.IO

So maybe there has been a tutorial going over this, but none of the ones I have read have addressed this issue for me. I have the structure as below and am trying to unit test, but when I go to test I always fails stating the repo method doSomthing() was never called. My best guess is because i have launched a new coroutine in a different context. How do I test this then?
Repository
interface Repository {
suspend fun doSomething(): String
}
View Model
class ViewModel(val repo: Repository) {
val liveData = MutableLiveData<String>()
fun doSomething {
//Do something here
viewModelScope.launch(Dispatchers.IO) {
val data = repo.doSomething()
withContext(Dispatchers.Main) {
liveData.value = data
}
}
}
}
View Model Test
class ViewModelTest {
lateinit var viewModel: ViewModel
lateinit var repo: Repository
#Before
fun setup() {
Dispatchers.setMain(TestCoroutineDispatcher())
repo = mock<Repository>()
viewModel = ViewModel(repo)
}
#Test
fun doSomething() = runBlockingTest {
viewModel.doSomething()
viewModel.liveData.test().awaitValue().assertValue {
// assert something
}
verify(repo).doSomthing()
}
}
According to Google:
Dispatchers should be injected into your ViewModels so you can properly test. You are setting the TestCorotutineDispatcher as the main Dispatcher via Dispatchers.setMain which takes control over the MainDispatcher, but you still have no control over the the execution of the coroutine launched via viewModelScope.launch(Dispatchers.IO).
Passing the Dispatcher via the constructor would make sure that your test and production code use the same dispatcher.
Typically an #Rule is defined that:
Overrides the MainDispatcher via Dispatchers.setMain (like you are doing)
Uses the TestCoroutineDispatcher's own runBlockingTest() to actually run the test.
Here is a really nice talk about testing and coroutines that happened at last year's Android Dev Summit.
And here is an example of such an #Rule. (Shameless plug. There are also examples of coroutine tests on that repo as well)
I write this solution for who use Dagger.
Inject CoroutineDispatcher in ViewModel constructor like this:
class LoginViewModel #Inject constructor(val dispatcher: CoroutineDispatcher) : BaseViewModel() {
and Provide Dispatcher like this:
#Singleton
#Provides
fun provideDispatchers(): CoroutineDispatcher = Dispatchers.IO
and in test package, Provide Dispatcher like this:
#Singleton
#Provides
fun provideDispatchers(): CoroutineDispatcher = UnconfinedTestDispatcher()
and now all lines in viewModelScope.launch(dispatcher) will be run

Testing coroutines in the presenter class

I'm struggling to test my presenter which is calling a suspended function from the repository layer as follow:
override fun viewCreated() {
launch {
val hasPermission = permissionChecker.execute() //suspended function
if (hasPermission) {
foo()
} else {
view.bar()
}
}
The presenter is also extending this interface:
interface CoroutinePresenter: CoroutineScope {
val job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
fun stopAllActiveJobs() {
coroutineContext.cancelChildren()
}
And the suspended function is defined as follow:
suspend fun execute() : Boolean = withContext(Dispatchers.IO) {
return#withContext class.foo()
}
Everything is working as expected in the app but when I tried to write some unit test I noticed that whenever I call the piece of code inside launch the thread is switched but the test doesn't wait for the execution. This is the implementation of the test:
#Test
fun `Test of Suspended Function`() = runBlocking {
presenter.viewCreated()
then(view).should().bar()
...
}
I also added the suggested library for testing kotlinx-coroutines-test but the result is still the same with it. I also tried to follow this suggestion and also implementing something like this but still no luck.
I think the problem is the actual creation of another thread whenever the launch is invoked in the presenter and the test doesn't actually know how to wait for it. I also tried to return a Job and invoking the job.join() but it fails with a NullPointerException.
Hope you guys can help me.
I found a solution for that:
following this tutorial, I've setup both
#Before
fun setup() {
Dispatchers.setMain(Dispatchers.Unconfined)
...
}
#After
fun tearDown() {
Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
}
And by running the entire launch block of the presenter class inside a runBlocking statement in the test. The problem was related also to a not reported exception inside the suspended function that was actually not mocked but it was invisible to my eyes.
Now everything is working fine.
Firstly, I strongly recommend that give your coroutineContext as a Parameter like that:
class CoroutinePresenter(coroutineContext: CoroutineContext): CoroutineScope {
init{
_coroutineContext = coroutineContext
}
override val coroutineContext: CoroutineContext
get() = _coroutineContext
// Your Methods
}
In your real environment:
#YourScope
#Provides
fun providesCoroutinePresenter(coroutineContext:CoroutineContext ){
return CoroutinePresenter()
}
#YourScope
#Provides
fun providesCoroutineContext(){
return Dispatchers.Main + job
}
During the unit test:
#Before
fun setUp() {
coroutinePresenter CoroutinePresenter(Dispatchers.Unconfined)
}
#Test
fun `Should do something`(){
//WHEN
coroutinePresenter.doSomething(params)
//THEN
do your assertions
}
For more please check SOLID Principles and for this case D

Categories

Resources