Простой тест для работы с сетью

Теория

Главный вопрос, который решает тестирование - это работает ли программа так, как было задумано изначально. Любое приложение представляет собой набор из функциональных блоков и разработчик должен быть уверен в том, что каждый работает ожидаемо. Наиболее ярко это может быть представлено в разрезе работы разных потоков с одним ресурсов и описывается в статье Гонка данных (Race Condition). Пример, описанный в статье, показывает как предсказуемый казалось бы результат на самом деле таковым не является, когда несколько потоков пытаются инкрементировать одну переменную.

Громадное количество приложений взаимодействуют с удаленными серверами, получая оттуда какие-то данные и обрабатывая их для пользователя. Корректная обработка удаленных данных, преобразование их в какие-то внутренние модели (маппинг) тоже являются местом для тестирования. Ниже мы рассмотрим как сделать самое простое тестирование такой обработки с использованием инструментов тестирования Robolectric и MockWebServer

Зависимости

Первым делом обеспечим наше приложение возможностью работать с Retrofit, OkHttp и остальными.

implementation("com.squareup.okio:okio:3.6.0")  
implementation("com.squareup.retrofit2:retrofit:2.9.0")  
implementation("com.squareup.okhttp3:okhttp:4.12.0")  
implementation("com.squareup.retrofit2:converter-gson:2.9.0")

Помимо стандартных библиотек, которые автоматически добавляются IDE, укажем еще набор нужных нам для решения наших задач.

testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")  
testImplementation("org.mockito:mockito-core:5.7.0")  
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")  
testImplementation("org.robolectric:robolectric:4.11.1")

Теоретически...

Добавим сюда немного теории.
Итак, у нас будет некоторый MockWebServer, который будет слушать наши запросы и возвращать какие-то ответы, при этом естественно никакие реальные запросы в сеть уходить не будут. Также нам надо создать интерфейс для нашего Api. И последнее, на что нам потребуется обратить внимание - это проследить как за созданием, так и отключением нашего вебсервера после завершения тестирования.

Пишем код

Интерфейс ApiTest.kt

interface ApiTest {  
    @GET("endpoint")  
    fun loadCharacters(): Call<List<CharacterResponse>>  
}

И что у нас возвращается

data class CharacterResponse(  
    @SerializedName("id") val id: String,  
    @SerializedName("name") val name: String  
)

Расширения для создания веб клиента

inline fun <reified T> createApi(mockWebServer: MockWebServer): T {  
    val okHttpClient = OkHttpClient.Builder()  
        .connectTimeout(1, TimeUnit.SECONDS)  
        .readTimeout(1, TimeUnit.SECONDS)  
        .writeTimeout(1, TimeUnit.SECONDS)  
        .build()  
  
    val gson = GsonBuilder()  
        .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZZ")  
        .create()  
  
    return Retrofit.Builder()  
        .baseUrl(mockWebServer.url("/"))  
        .client(okHttpClient)  
        .addConverterFactory(GsonConverterFactory.create(gson))  
        .build()  
        .create(T::class.java)  
}

В этом месте наши реализации могут отличаться, я в общем виде описал процесс создания Retrofit клиента. Обратите внимание на использование reified, который позволил нам немного сократить сигнатуру функции - в противном случае нам пришлось бы параметром передавать тип нашего Api интерфейса, например cl = ApiTest::class

Создание веб сервера

internal fun createMockServer(): MockWebServer {  
    val mockWebServer = MockWebServer()  
    mockWebServer.start(8080)  
    mockWebServer.dispatcher = object : Dispatcher() {  
        override fun dispatch(request: RecordedRequest): MockResponse {  
            return MockResponse().setResponseCode(200).setBody("[]")  
        }  
    }  
  
    return mockWebServer  
}

В принципе тут все довольно понятно, но если нет, то ниже описание принципа работы. Веб сервер слушает все запросы и в методе dispatch перехватывает их и возвращает определенный ответ. В нашем случае никакой гибкости нет - он всегда возвращает код 200 и ответ в виде пустого массива [].

Unit тест

@RunWith(RobolectricTestRunner::class)  
@Config(manifest = Config.NONE)  
class SimpleApiTest {  
    private lateinit var mockWebServer: MockWebServer  
    private lateinit var api: ApiTest  
  
    @Before  
    fun setup() {  
        mockWebServer = createMockServer()  
        api = createApi<ApiTest>(mockWebServer)  
    }  
  
    @After  
    fun shutdown() {  
        mockWebServer.shutdown()  
    }  
  
    @Test  
    fun makeApiCall() {  
        val response = api.loadCharacters().execute()  
        Assert.assertEquals(emptyList<Any>(), response.body())  
    }  
}
  • В методе setup создали и запустили веб сервер, а также создали Retrofit клиента для нашего интерфейса
  • В методе shutdown мы отключаем вебсервер.
  • makeApiCall делаем запрос через метод loadCharacters() и ожидаем, что у нас всегда возвращается пустой массив

Это самый простой пример для тестирования веб интерфейсов. Дальше мы рассмотрим как сделать сервер более гибким.