NetMock: простой подход к тестированию HTTP-запросов в Java, Android и Kotlin Multiplatform

Представляю NetMock  —  это мощная и удобная библиотека, которая упрощает имитацию HTTP-запросов и ответов.

Код NetMock

Тестирование HTTP-запросов часто представляет трудности для разработчиков из-за сложности имитации запросов и ответов в тестовых средах. Вследствие этого увеличивается время и количество ресурсов, затраченных на ручное тестирование. Столкнувшись с подобными проблемами при работе с существующими HTTP-библиотеками и громоздкими инструментами для имитации, я разработал собственное решение: NetMock.

NetMock легко и просто интегрируется с Java, Android и Kotlin Multiplatform. С минимальными настройками вы можете без труда создавать имитированные реализации, которые точно воспроизводят клиентские запросы и реальные ответы конечных точек. Одно из главных преимуществ NetMock состоит в последовательном подходе к имитации HTTP-запросов и ответов. Такой подход особенно эффективен при работе над проектами, задействующими разные библиотеки, или при переходе от одной библиотеки к другой. 

Еще одной примечательной особенностью NetMock является удобный дизайн. В отличие от многих инструментов для имитации HTTP NetMock придерживается принципов простоты использования. Библиотека предлагает понятный интерфейс и легкий процесс установки. Она заинтересует разработчиков, желающих расширить свои познания HTTP-библиотек. 

Библиотека NetMock  —  это отличное решение как для опытного, так и для начинающего разработчика. Она упрощает рабочий процесс и сокращает временные/физические затраты, необходимые для создания надежного ПО. 

Продолжить знакомство с NetMock можно по ссылке на репозиторий GitHub.

Улучшение стратегии тестирования HTTP-кода 

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

В различных проектах мне встречался один подход. Он предполагает изолирование HTTP-кода в отдельные компоненты, что позволяет проводить независимое тестирование остальной системы. Несмотря на то, что данный подход упрощает репозитории и тестовые классы, он часто пренебрегает тестированием самого HTTP-кода из-за сопутствующих сложностей. В результате возникает потребность проверять важнейшую логику, связанную с HTTP-запросами и парсингом JSON.

Данная проблема прослеживается в проектах, задействующих Retrofit. Эта библиотека абстрагирует HTTP-логику посредством интерфейсов. Рассмотрим следующий фрагмент кода:

interface GitHubService {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): List<RepoDto>
}

class GitHubRepositoryImpl(
private val githubService: GitHubService,
private val repoMapper: RepoMapper
): GitHubRepository {
override suspend fun listRepos(user: String): List<Repo> {
val repoDtos = githubService.listRepos(user)
return repoMapper.map(repoDtos)
}
}

Хотя этот подход упрощает репозитории и тестовые классы, он часто пренебрегает важными тестами для HTTP-логики. Кроме того, существует вероятность создания ненужных компонентов исключительно для упрощения модульного тестирования репозиториев.

Цель модульного тестирования  —  предотвращать и выявлять ошибки разработчиков, тем самым гарантируя правильную работу кода. Без надлежащих тестов даже простые ошибки, такие как изменение пути с users/{user}/repos на user/{user}/repos или переименование поля в RepoDto, могут остаться незамеченными, что неприемлемо в профессиональном проекте. 

Я вовсе не критикую Retrofit. Наоборот, считаю ее отличной и удобочитаемой библиотекой. 

Более того, признаю целесообразность изолирования HTTP-кода в отдельные компоненты ради улучшения структуры и удобства сопровождения.

Я просто указываю на недостатки стратегии тестирования, которую наблюдал во многих проектах. Она вынуждает больше внимания уделять комплексному тестированию HTTP-кода, вследствие чего потенциальные проблемы остаются незамеченными. Акцентируя данную проблему, призываю разработчиков с особой ответственностью подходить к тестированию HTTP-логики, тем самым обеспечивая общее качество и надежность проектов.

Имитация запросов и ответов с помощью NetMock 

@Test
fun `your test`() = runTest {
val user = "some_user"
netMock.addMock(
request = {
method = Method.Get
requestUrl = "https://api.github.com/users/$user/repos"
},
response = {
code = 200
body = readFromResources("responses/repo_list.json")
}
)

val result = sut.listRepos(user)

//...
}

Для имитации запросов и ответов применяется простой API, предоставляемый NetMock.

Добавляя mock-объект в очередь ожидаемых запросов и ответов, вы можете перехватывать и контролировать поведение HTTP-взаимодействий во время тестирования. 

Создание экземпляра NetMock 

NetMock предлагает 2 варианта: netmock-server и netmock-engine.

Вариант netmock-server совместим с Java, Kotlin, Android и не зависит от библиотеки. Он позволяет имитировать сетевые запросы, перенаправляя их на веб-сервер localhost с помощью MockWebServer. Данный вариант идеально подходит для разработчиков, которые создают проекты без Kotlin Multiplatform и тестируют сетевые запросы без настройки отдельного сервера.

Вариант netmock-engine предназначен для разработчиков, задействующих Ktor или Kotlin Multiplatform. Он изначально использует MockEngine вместо сервера localhost. Эта особенность делает его легким и мультиплатформенным вариантом для тех, кто работает с Ktor.

Применение NetMockServer

Чтобы добавить netmock-server в проект, пополняем зависимости в файле build.gradle:

dependencies {
testImplementation "io.github.denisbronx.netmock:netmock-server:0.4.0"
}

Затем создаем экземпляр NetMockServer:

@get:Rule
val netMock = NetMockServerRule()

NetMockServer запускает сервер localhost, перехватывающий запросы с помощью MockWebServer. NetMockServerRule управляет жизненным циклом севера, автоматически его запуская и отключая для каждого теста.

После запуска сервера вы можете настроить код для указания на базовый URL-адрес localhost с помощью netmock.baseUrl.

Рассмотрим примеры Retrofit и Ktor.

Пример Retrofit с localhost:

@get:Rule
val netMock = NetMockServerRule()

private val service = Retrofit.Builder()
.baseUrl(netMock.baseUrl)
.build()
.create(GitHubService::class.java)

Теперь соответствующий пример Ktor:

@get:Rule
val netMock = NetMockServerRule()

private val client = HttpClient(OkHttp) {
defaultRequest {
url(netMock.baseUrl)
}
}

Как вариант, можно добавить перехватчик с помощью netmock.interceptor, который автоматически перенаправляет запросы на localhost.

Пример Retrofit с реальным URL-адресом:

@get:Rule
val netMock = NetMockServerRule()

private val service = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.client(OkHttpClient.Builder().addInterceptor(netMock.interceptor).build())
.build()
.create(GitHubService::class.java)

Аналогичный пример Ktor:

@get:Rule
val netMock = NetMockServerRule()

private val client = HttpClient(OkHttp) {
engine {
addInterceptor(netMock.interceptor)
}
}

Рекомендуется использовать перехватчик, поскольку он позволяет работать с реальными и динамическими URL-адресами. 

Примечание. Потребуется библиотека, совместимая с перехватчиками OkHttp, например OkHttp, Retrofit или Ktor.

Заключение 

За исключением процесса инициализации оба варианта NetMock в целом идентичны. Это означает, что переход между netmock-server и netmock-engine требует минимальных изменений. Такая гибкость позволяет легко переключаться между разными библиотеками, например между Retrofit и Ktor, без изменений тестов. 

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

Таким образом, универсальность NetMock позволяет адаптировать тестовые настройки под разные библиотеки и облегчает рефакторинг, тем самым повышая надежность и удобство сопровождения базы кода. 

Надеюсь, предоставленный материал выявил всю важность комплексного тестирования на сетевом уровне.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Denis Brandi: Introducing NetMock: Simplifying HTTP Request Testing in Java, Android, and Kotlin Multiplatform

Предыдущая статьяСопоставление LiveData, SingleLiveEvent и MediatorLiveData в Android
Следующая статьяСоздание анимированной кнопки-счетчика в Jetpack Compose