макет модернизировать приостановить функцию бесконечный ответ - PullRequest
3 голосов
/ 25 февраля 2020

Я хотел бы проверить, когда сервер не возвращает ответ, и мы запускаем следующий сетевой вызов (как, например, поисковый запрос).

Таким образом, у нас есть метод внутри ViewModel и метод Retrofit

  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

        }
    }
}

И я хотел бы проверить, когда новый запрос запрашивается, а старый не возвращается. для случая, когда ответ возвращает тест, это просто

@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
        }
    }

Но я не знаю, как смоделировать функцию приостановки, которая не вернулась, и тестовый случай, когда новый запрос запускается, как это

@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
        }
    }

Есть идеи?

РЕДАКТИРОВАТЬ: Я не очень привязан к mockito, любая фиктивная библиотека будет хороша :) привет Wojtek

Ответы [ 2 ]

2 голосов
/ 28 февраля 2020

Я придумал какое-то решение проблемы, но немного отличается от того, о чем я думал в начале

        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)
            }
        }
    }

И сценарий теста будет выглядеть так

        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)
            }
        }
    }

Это не совсем то, что я хотел, потому что при первом вызове метода load вызов дооснащения не запускается, но это самое близкое решение.

Каким будет идеальный тест для меня, будет утверждение эта модификация была вызвана дважды, но только вторая вернулась в ViewModel. Решением для этого будет обернуть Retrofit вокруг метода, который возвращает функцию приостановки, подобную этой

    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)
            }
        }
    }

, и проверить ее

    @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()
        }
    }

Но, на мой взгляд, это слишком много в вмешательство в код только для проверки. Но, возможно, я ошибаюсь: P

0 голосов
/ 28 февраля 2020

Похоже, вы хотите протестировать сценарий, когда у вас недоступный сервер, время ожидания или что-то подобное.

В этом случае, выполняя макет, вы можете сказать, что при первой попытке он возвращает объект, а затем при втором выполнении выдает исключение, например java.net.ConnectException: Connection timed out.

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

И это должно работать, но вам придется делать блок try / catch в ViewModel, который не идеален. Я бы предложил вам добавить дополнительную абстракцию.

Вы могли бы Repository или UseCase или любой другой шаблон / имя, которое вам нравится, для перемещения туда сетевого вызова. Затем введите sealed class Result, чтобы инкапсулировать поведение и сделать ваш ViewModel более читабельным.

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
}

При таком уровне абстракции ViewModelTest проверяет только то, что происходит в двух случаях.

Надежда это полезно!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...