Тестирование сети и использование силы Mock

В предыдущей статье мы написали почти полностью готовый функционал по юнит тестированию функций, которые работают с сетью. В этой мы быстро затронем как можно "замокать" некоторые функции в репозитории.

Для чего?

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

Дополняем код

Мы добавим еще один Source для чтения данных локально, например, из базы данных. Но его реализацию писать не будем, этого нам не требуется в рамках задачи по пониманию как "замокать" функционал.

interface LocalSource {  
    fun getLocalDisneyCharacters(): List<CartoonCharacter>  
}

Этот класс по работе с локальными данными мы добавим в наш репозиторий

internal class CharacterRepositoryImpl(  
    private val remoteSource: RemoteSource,  
    private val localSource: LocalSource,  
) : CharacterRepository {  
  
    override fun getRemoteCharacters(): List<CartoonCharacter> {  
        return remoteSource.getDisneyCharacters()  
    }  
  
    override fun getLocalCharacters(): List<CartoonCharacter> {  
        return localSource.getLocalDisneyCharacters()  
    }  
}

Тестируем и мокаем правильно

В общем случае есть два способа как замокать функционал. Первый - это замокать вообще весь класс с использованием Mockito.mock(). Второй способ чуть более сложный, тесты с ним надо писать аккуратно, чтобы не совершить никаких ошибок (то есть условие Assert.assertEquals должно всегда выполняться)

Мокаем вообще все

Изменим настройку нашего теста в методе setup

@Before  
    fun setup() {  
        characterRepository = Mockito.mock(CharacterRepository::class.java)    }

Мы сообщили Mockito что сами будем определять что будет возвращаться в разных функциях, вот, например, как ниже

@Test  
fun makeMockWholeClass(){  
    val mockResult = listOf(CartoonCharacter(id = "5", name = "Donald Duck", universe = "Mock"))  
    `when`(characterRepository.getLocalCharacters()).thenReturn(mockResult)  
    val list = characterRepository.getLocalCharacters()  
    val id1 = list.firstOrNull()?.id  
    Assert.assertEquals("5", id1)  
}

С помощью Mockito.when мы говорим что должно вернуться при вызове функции getLocalCharacters.

Мокаем частично

Снова меняем функцию настройки setup. Нам нужен функционал для имитации запросов в сеть, а также мы мокаем класс LocalSource. На самом деле здесь это необязательно, можно было бы написать val localSource = object : LocalSource... потому что в примере это практически не используется.

@Before  
fun setup() {  
    mockWebServer = createMockServer { path, method ->  
        when (path) {  
            "/characters" -> "response_characters"  
            "/episodes" -> "response_episodes"  
            else -> null  
        }  
    }  
  
    val api = createApi<ApiTest>(mockWebServer)  
    val remoteSource = OkRemoteSourceImpl(api)  
    val localSource = Mockito.mock(LocalSource::class.java)  
    characterRepository = CharacterRepositoryImpl(remoteSource, localSource)  
}

Я хочу в методе makeMockCallWithSpy протестировать как замоканный функционал, так и обработку ответа с сервера.

@Test  
fun makeMockCallWithSpy() {  
    val spyRepository = Mockito.spy(characterRepository)  
    val mockResult = listOf(CartoonCharacter(id = "10", name = "Goofy", universe = "Mock"))  
    `when`(spyRepository.getLocalCharacters()).thenReturn(mockResult)  
  
    val list = spyRepository.getLocalCharacters()  
    val id1 = list.firstOrNull()?.id  
    Assert.assertEquals("10", id1)  
  
    val remoteList = spyRepository.getRemoteCharacters()  
    val id2 = remoteList.firstOrNull()?.id  
  
    Assert.assertEquals("1", id2)  
}

Мы указываем, что объект spyRepository следит за реальным объектом characterRepository и может некоторые функции переопределять. Тут надо обратить внимание на то, что если мы указали, что Mockito будет следить за каким-то классом, то все действия и проверки мы будем делать уже над объектом слежения.

В нашем случае им является spyRepository. Мы говорим, что когда у него будет вызываться метод getLocalCharacters, требуется вернуть определенный список объектов. Далее мы вызываем обе функции - замоканную и нет - и проверяем результаты.