Простой тест для работы с сетью
Главный вопрос, который решает тестирование - это работает ли программа так, как было задумано изначально. Любое приложение представляет собой набор из функциональных блоков и разработчик должен быть уверен в том, что каждый работает ожидаемо. Наиболее ярко это может быть представлено в разрезе работы разных потоков с одним ресурсов и описывается в статье Гонка данных (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. И последнее, на что нам потребуется обратить внимание - это проследить как за созданием, так и отключением нашего вебсервера после завершения тестирования.
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 и ответ в виде пустого массива []
.
@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()
и ожидаем, что у нас всегда возвращается пустой массивЭто самый простой пример для тестирования веб интерфейсов. Дальше мы рассмотрим как сделать сервер более гибким.