mock retrofit suspend function infinite response

Viewed 1430

I would like to test case when server does not return response, and we trigger the next network call ( like for example search query).

So we basically have a method inside ViewModel and Retrofit method

  interface RetrofitApi {
    @GET("Some Url")
    suspend fun getVeryImportantStuff(): String
}

class TestViewModel(private val api: RetrofitApi) : ViewModel() {

    private var askJob: Job? = null
    fun load(query: String) {
        askJob?.cancel()
        askJob = viewModelScope.launch {
            val response = api.getVeryImportantStuff()

            //DO SOMETHING WITH RESPONSE

        }
    }
}

And I would like to test case when new query is asked, and the old one didn't returns. for case when response returns test is easy

@Test
    fun testReturnResponse() {
        runBlockingTest {
            //given
            val mockApi:RetrofitApi = mock()
            val viewModel = TestViewModel(mockApi)
            val response = "response from api"

            val query = "fancy query"
            whenever(mockApi.getVeryImportantStuff()).thenReturn(response)

            //when
            viewModel.load(query)


            //then
            //verify what happens
        }
    }

But I don't know how to mock suspend function that did't come back, and test case when new request is triggered like this

@Test
    fun test2Loads() {
        runBlockingTest {
            //given
            val mockApi:RetrofitApi = mock()
            val viewModel = TestViewModel(mockApi)
            val response = "response from api"
            val secondResponse = "response from api2"

            val query = "fancy query"
            whenever(mockApi.getVeryImportantStuff())
                .thenReturn(/* Here return some fancy stuff that is suspend* or something like onBlocking{} stub but not  blocking but dalayed forever/)
                .thenReturn(secondResponse)

            //when
            viewModel.load(query)
            viewModel.load(query)


            //then
            //verify that first response did not happens , and only second one triggered all the stuff
        }
    }

Any ideas ?

EDIT: I'm not really attached to mockito, any mock library will be good :) regards Wojtek

2 Answers

I came up with kind of solution to the problem, but slightly different than I was thinking at the beginning

        interface CoroutineUtils {
            val io: CoroutineContext
        }

        interface RetrofitApi {
            @GET("Some Url")
            suspend fun getVeryImportantStuff(query: String): String
        }

        class TestViewModel(private val api: RetrofitApi,
                            private val utils: CoroutineUtils) : ViewModel() {
        private val text = MutableLiveData<String>()
        val testStream: LiveData<String> = text
        private var askJob: Job? = null
        fun load(query: String) {
            askJob?.cancel()
            askJob = viewModelScope.launch {
                val response = withContext(utils.io) { api.getVeryImportantStuff(query) }
                text.postValue(response)
            }
        }
    }

And the test scenario would look like this

        class TestViewModelTest {

        @get:Rule
        val coroutineScope = MainCoroutineScopeRule()
        @get:Rule
        val instantTaskExecutorRule = InstantTaskExecutorRule()


        lateinit var retrofit: RetrofitApi

        lateinit var utils: CoroutineUtils

        val tottalyDifferentDispatcher = TestCoroutineDispatcher()

        lateinit var viewModel: TestViewModel
        @Before
        fun setup() {
            retrofit = mock()
            utils = mock()
            viewModel = TestViewModel(retrofit, utils)
        }


        @UseExperimental(ExperimentalCoroutinesApi::class)
        @Test
        fun test2Loads() {
            runBlockingTest {
                //given
                val response = "response from api"
                val response2 = "response from api2"
                val query = "fancy query"
                val query2 = "fancy query2"

                whenever(utils.io)
                    .thenReturn(tottalyDifferentDispatcher)

                val mutableListOfStrings = mutableListOf<String>()

                whenever(retrofit.getVeryImportantStuff(query)).thenReturn(response)
                whenever(retrofit.getVeryImportantStuff(query2)).thenReturn(response2)

                //when

                viewModel.testStream.observeForever {
                    mutableListOfStrings.add(it)
                }
                tottalyDifferentDispatcher.pauseDispatcher()
                viewModel.load(query)
                viewModel.load(query2)

                tottalyDifferentDispatcher.resumeDispatcher()

                //then
                mutableListOfStrings shouldHaveSize 1
                mutableListOfStrings[0] shouldBe response2
                verify(retrofit, times(1)).getVeryImportantStuff(query2)
            }
        }
    }

It is not exactly what I wanted, because retrofit call is not triggered when load method is called for the first time, but it is the closest solution.

What would be a perfect test for me will be assertion that retrofit was called twice , but only the second one returned to ViewModel. Solution for that will be to wrap Retrofit around method that returns suspend function like this

    interface RetrofitWrapper {
     suspend fun getVeryImportantStuff(): suspend (String)->String
    }
    class TestViewModel(private val api: RetrofitWrapper,
                        private val utils: CoroutineUtils) : ViewModel() {

        private val text = MutableLiveData<String>()
        val testStream: LiveData<String> = text
        private var askJob: Job? = null
        fun load(query: String) {
            askJob?.cancel()
            askJob = viewModelScope.launch {
                val veryImportantStuff = api.getVeryImportantStuff()
                val response = withContext(utils.io) {
                    veryImportantStuff(query)
                }
                text.postValue(response)
            }
        }
    }

and test for it

    @Test
    fun test2Loads() {
        runBlockingTest {
            //given
            val response = "response from api"
            val response2 = "response from api2"
            val query = "fancy query"
            val query2 = "fancy query2"

            whenever(utils.io)
                .thenReturn(tottalyDifferentDispatcher)

            val mutableListOfStrings = mutableListOf<String>()

            whenever(retrofit.getVeryImportantStuff())
                .thenReturn(suspendCoroutine {
                    it.resume { response }
                })
            whenever(retrofit.getVeryImportantStuff()).thenReturn(suspendCoroutine {
                it.resume { response2 }
            })

            //when

            viewModel.testStream.observeForever {
                mutableListOfStrings.add(it)
            }
            tottalyDifferentDispatcher.pauseDispatcher()
            viewModel.load(query)
            viewModel.load(query2)

            tottalyDifferentDispatcher.resumeDispatcher()

            //then
            mutableListOfStrings shouldHaveSize 1
            mutableListOfStrings[0] shouldBe response2
            verify(retrofit, times(2)).getVeryImportantStuff()
        }
    }

But in my opinion it is a little bit too much in interference in code only to be testable. But maybe I'm wrong :P

Looks like you want to test scenario when you have unreachable server, timeout or something similar.

In this case while doing your mock you can say that on first try it returns object and then on second executions throws exception like java.net.ConnectException: Connection timed out.

                whenever(mockApi.getVeryImportantStuff())
                .thenReturn(someObjet)
                .thenThrow(ConnectException("timed out"))

And this this should work but you will have to do try/catch block in ViewModel witch is not ideal. I would suggest you to add additional abstraction.

You could you Repository or UseCase or whatever pattern/name you like to move the network call there. Then introduce sealed class Result to encapsulate behaviour and make your ViewModel more readable.

class TestViewModel(val repo: Repo): ViewModel() {
    private var askJob: Job? = null

    fun load(query: String) {
        askJob?.cancel()
        askJob = viewModelScope.launch {
            when (repo.getStuff()) {
                is Result.Success -> TODO()
                is Result.Failure -> TODO()
            }
        }
    }
}

class Repo(private val api: Api) {
    suspend fun getStuff() : Result {
        return try {
            Result.Success(api.getVeryImportantStuff())
        } catch (e: java.lang.Exception) {
            Result.Failure(e)
        }
    }
}

sealed class Result {
    data class Success<out T: Any>(val data: T) : Result()
    data class Failure(val error: Throwable) : Result()
}

interface Api {
    suspend fun getVeryImportantStuff() : String
}

With that level of abstraction your ViewModelTest only checks what happens in two cases.

Hope that's helpful!

Related