Работая над последним проектом, столкнулся с тестированием мобильного приложения, связанного на уровне бизнес-логики с различными сторонними сервисами. Тестирование этих сервисов не входило в мою задачу, однако проблемы с их API блокировали работу по самому приложению – тесты падали не из-за проблем внутри, а из-за неработоспособности API, даже не доходя до проверки нужной функциональности.
Традиционно для тестирования таких приложений используются стенды. Но они не всегда работают нормально, и это мешает работе. В качестве альтернативного решения я использовал моки. Об этом тернистом пути и хочу рассказать сегодня.

Чтобы не трогать код реального проекта (под NDA), для наглядности дальнейшего изложения я создал простой REST-клиент под Android, позволяющий отправлять на некий адрес HTTP-запросы (GET/POST) с необходимыми мне параметрами. Его-то мы и будем тестировать.
Код приложения-клиента, диспатчеров и тестов можно скачать с GitLab.
Подходов к мокированию в моем случае существовало два:
Первый вариант несильно отличается от тестового стенда. Действительно, можно выделить под мок-сервер рабочее место в сети, но его необходимо будет поддерживать, как и любой тестовый стенд. Вот тут-то и придется столкнуться с основными подводными камнями этого подхода. Удаленное рабочее место умерло, перестало отвечать, что-то поменялось – надо следить, менять конфигурацию, т.е. делать все то же самое, что и при поддержке обычного тестового стенда. Ситуацию для себя мы никак не исправляем, и на это точно уйдет больше времени, чем на любые локальные манипуляции. Так что конкретно в моем проекте было удобнее поднимать мок-сервер локально.
Разных инструментов существует много. Я пытался работать с несколькими и почти в каждом столкнулся с определенными проблемами:
Текущая версия okhttpmockwebserver позволяет реализовать несколько сценариев работы:
Рассмотрим каждый из сценариев подробнее.
Простейшая реализация мок-сервера – очередь ответов. До теста я определяю адрес и порт, где будет развернут мок-сервер, а также тот факт, что он будет работать по принципу очереди из сообщений – FIFO (first in first out).
Далее запускаю мок-сервер.
Тесты написаны с помощью фреймворка Espresso, предназначенного для исполнения действий в мобильных приложениях. В этом примере я выбираю типы запросов и отправляю их по очереди.
После запуска теста мок-сервер дает ему ответы в соответствии с прописанной очередью, и тест проходит без ошибок.
Диспатчер – это набор правил, по которым работает мок-сервер. Для удобства изложения я создал три разных диспатчера: SimpleDispatcher, OtherParamsDispatcher и ListingDispatcher.
Для реализации диспатчера okhttpmockwebserver предоставляет класс
Логика в этом примере простая: если приходит GET, я возвращаю сообщение, что это GET request. Если POST, возвращаю сообщение о POST request. В иных ситуациях возвращаю пустой запрос.
В тесте появляется
Исходники тестов с
Переопределяя функцию
В данном случае я демонстрирую несколько вариантов условий.
Во-первых, в API можно передавать параметры в адресной строке. Поэтому я могу поставить условие на вхождение в path какой-либо связки, например
Во-вторых, данный класс позволяет залезть внутрь тела (body) запросов POST или PUT. Например, можно использовать
За примерами тестов рекомендую обратиться к репозиторию.
При необходимости можно реализовать и более сложную логику – ListingDispatcher. Тем же способом я переопределяю функцию
Для этого я создал открытый класс
В результате я могу строить более сложные сочетания условий для стабов. Мне эта конструкция показалась более гибкой, хотя принцип в ее основе очень простой.
Но более всего в этом подходе мне понравилось, что я могу подставлять какие-то стабы прямо на ходу, если появилась необходимость что-то поменять в ответе мок-сервера на одном тесте. При тестировании больших проектов такая задача возникает довольно часто, например при проверке каких-то специфических сценариев.
Замену можно осуществить следующим образом:
При такой реализации диспатчера тесты остаются простыми. Я также стартую мок-сервер, только выбираю
Ради эксперимента я заменил стаб на POST:
Для этого вызвал функцию
В тесте выше я проверяю, что стаб был заменен.
Затем я добавил новый стаб, которого не было в дефолтных.
Последний кейс – Request verifier – обеспечивает не мокирование, а проверку отправляемых приложением запросов. Для этого я точно так же стартую мок-сервер, реализовав диспатчер, чтобы приложение возвращало хоть что-то.
При отправке запроса из теста тот приходит в мок-сервер. Через него я могу получить доступ к параметрам запроса, используя
Выше я показал проверку на простом примере. Точно такой же подход можно использовать для сложных JSON, в том числе для проверки всей структуры запроса (можно сравнивать на уровне JSON или распарсить JSON на объекты и проверить равенство на уровне объектов).
В целом инструмент (okhttpmockwebserver) мне понравился, и я использую его на большом проекте. Безусловно, есть некоторые мелочи, которые я хотел бы изменить.
Например, мне не нравится, что приходится стучаться по локальному адресу (localhost:8080 в нашем примере) в конфигах своего приложения; возможно, я еще найду способ все настроить так, чтобы мок-сервер отвечал при попытке отправить запрос на любой адрес.
Также мне не хватает возможности переадресации запросов – когда мок-сервер отправляет запрос дальше, если у него нет для него подходящего стаба. В данном мок-сервере такого подхода нет. Впрочем, до их внедрения и не дошло, поскольку на данный момент в “боевом” проекте не стоит такой задачи.
Автор статьи: Руслан Абдулин
P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на наши страницы в VK, FB или Telegram-канал, чтобы узнавать обо всех наших публикациях и других новостях компании Maxilect.
Традиционно для тестирования таких приложений используются стенды. Но они не всегда работают нормально, и это мешает работе. В качестве альтернативного решения я использовал моки. Об этом тернистом пути и хочу рассказать сегодня.

Чтобы не трогать код реального проекта (под NDA), для наглядности дальнейшего изложения я создал простой REST-клиент под Android, позволяющий отправлять на некий адрес HTTP-запросы (GET/POST) с необходимыми мне параметрами. Его-то мы и будем тестировать.
Код приложения-клиента, диспатчеров и тестов можно скачать с GitLab.
Какие существуют варианты?
Подходов к мокированию в моем случае существовало два:
- развернуть мок-сервер в облаке или на удаленной машине (если речь идет о конфиденциальных разработках, которые нельзя выносить в облако);
- запускать мок-сервер локально – прямо на телефоне, на котором тестируется мобильное приложение.
Первый вариант несильно отличается от тестового стенда. Действительно, можно выделить под мок-сервер рабочее место в сети, но его необходимо будет поддерживать, как и любой тестовый стенд. Вот тут-то и придется столкнуться с основными подводными камнями этого подхода. Удаленное рабочее место умерло, перестало отвечать, что-то поменялось – надо следить, менять конфигурацию, т.е. делать все то же самое, что и при поддержке обычного тестового стенда. Ситуацию для себя мы никак не исправляем, и на это точно уйдет больше времени, чем на любые локальные манипуляции. Так что конкретно в моем проекте было удобнее поднимать мок-сервер локально.
Выбор мок-сервера
Разных инструментов существует много. Я пытался работать с несколькими и почти в каждом столкнулся с определенными проблемами:
- Mock-server, wiremock – два мок-сервера, которые я так и не смог нормально запустить на Android. Поскольку все эксперименты происходили в рамках живого проекта, время на выбор было ограничено. Поковырявшись с ними пару дней, я оставил попытки.
- Restmock – это обертка над okhttpmockwebserver, подробнее о котором речь пойдет далее. Выглядела она неплохо, запустилась, но разработчик этой обертки спрятал “под капотом” возможность задания IP-адреса и порта мок-сервера, а для меня это было критично. Restmock стартовал на каком-то случайном порту. Ковыряясь в коде, я увидел, что при инициализации сервера разработчик использовал метод, который задавал порт случайным образом, если не получал его на вход. В принципе, можно было наследоваться от этого метода, но проблема была в приватном конструкторе. В итоге от обертки я отказался.
- Okhttpmockwebserver – попробовав разные инструменты, я остановился на мок-сервере, который нормально собрался и запустился локально на устройстве.
Разбираем принцип работы
Текущая версия okhttpmockwebserver позволяет реализовать несколько сценариев работы:
- Очередь ответов. Ответы мок-сервера складываются в очередь, работающую по принципу FIFO. Неважно, к какому API и по какому пути я буду обращаться, мо��-сервер по очереди будет выкидывать сообщения, заложенные в эту очередь.
- Диспатчер позволяет создать правила, определяющие, какой ответ отдавать. Допустим, запрос пришел по URL, содержащему некий путь, например /get-login/. По этому /get-login/ мок-сервер и отдает единичный, заранее заданный ответ.
- Request Verifier. Опираясь на предыдущий сценарий, я могу проверять запросы, которые отправляет приложение (что в заданных условиях запрос с определенными параметрами действительно уходит). При этом ответ неважен, поскольку он определяется тем, как работает API. Этот сценарий и реализует Request verifier.
Рассмотрим каждый из сценариев подробнее.
Очередь ответов
Простейшая реализация мок-сервера – очередь ответов. До теста я определяю адрес и порт, где будет развернут мок-сервер, а также тот факт, что он будет работать по принципу очереди из сообщений – FIFO (first in first out).
Далее запускаю мок-сервер.
class QueueTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.enqueue(MockResponse().setBody("1st message")) mockServer.enqueue(MockResponse().setBody("2nd message")) mockServer.enqueue(MockResponse().setBody("3rd message")) mockServer.start(ip, port) } @Test fun queueTest() { sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("1st message") returnFromResponseActivity() sendPostRequest("http://localhost:8080/getMessage") assertResponseMessage("2nd message") returnFromResponseActivity() sendGetRequest("http://localhost:8080/getMessage") assertResponseMessage("3rd message") returnFromResponseActivity() } }
Тесты написаны с помощью фреймворка Espresso, предназначенного для исполнения действий в мобильных приложениях. В этом примере я выбираю типы запросов и отправляю их по очереди.
После запуска теста мок-сервер дает ему ответы в соответствии с прописанной очередью, и тест проходит без ошибок.
Реализация диспатчера
Диспатчер – это набор правил, по которым работает мок-сервер. Для удобства изложения я создал три разных диспатчера: SimpleDispatcher, OtherParamsDispatcher и ListingDispatcher.
SimpleDispatcher
Для реализации диспатчера okhttpmockwebserver предоставляет класс
Dispatcher(). От него можно наследоваться, переопределив функцию dispatch по-своему.class SimpleDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { if (request.method == "GET"){ return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request" }""") } else if (request.method == "POST") { return MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request" }""") } return MockResponse().setResponseCode(200) } }
Логика в этом примере простая: если приходит GET, я возвращаю сообщение, что это GET request. Если POST, возвращаю сообщение о POST request. В иных ситуациях возвращаю пустой запрос.
В тесте появляется
dispatcher – объект класса SimpleDispatcher, который я описал выше. Далее, как и в предыдущем примере, запускается мок-сервер, только на этот раз указывается своего рода правило работы с этим мок-сервером – тот самый диспатчер.Исходники тестов с
SimpleDispatcher можно найти в репозитории.OtherParamsDispatcher
Переопределяя функцию
dispatch, я могу оттолкнуться от других параметров запроса для отправки ответов:class OtherParamsDispatcher: Dispatcher() { @Override override fun dispatch(request: RecordedRequest): MockResponse { return when { request.path.contains("?queryKey=value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a GET request with query parameter queryKey equals value" }""") request.body.toString().contains("\"bodyKey\":\"value\"") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was a POST request with body parameter bodyKey equals value" }""") request.headers.toString().contains("header: value") -> MockResponse().setResponseCode(200).setBody("""{ "message": "It was some request with header equals value" }""") else -> MockResponse().setResponseCode(200).setBody("""{ Wrong response }""") } } }
В данном случае я демонстрирую несколько вариантов условий.
Во-первых, в API можно передавать параметры в адресной строке. Поэтому я могу поставить условие на вхождение в path какой-либо связки, например
“?queryKey=value”.Во-вторых, данный класс позволяет залезть внутрь тела (body) запросов POST или PUT. Например, можно использовать
contains, предварительно выполнив toString(). В моем примере условие срабатывает, когда приходит POST-запрос, содержащий “bodyKey”:”value”. Аналогично я могу валидировать header запроса (header : value).За примерами тестов рекомендую обратиться к репозиторию.
ListingDispatcher
При необходимости можно реализовать и более сложную логику – ListingDispatcher. Тем же способом я переопределяю функцию
dispatch. Однако теперь прямо в классе задаю дефолтный набор стабов (stubsList) – моков на разные случаи жизни.class ListingDispatcher: Dispatcher() { private var stubsList: ArrayList<RequestClass> = defaultRequests() @Override override fun dispatch(request: RecordedRequest): MockResponse = try { stubsList.first { it.matcher(request.path, request.body.toString()) }.response() } catch (e: NoSuchElementException) { Log.e("Unexisting request path =", request.path) MockResponse().setResponseCode(404) } private fun defaultRequests(): ArrayList<RequestClass> { val allStubs = ArrayList<RequestClass>() allStubs.add(RequestClass("/get", "queryParam=value", "", """{ "message" : "Request url starts with /get url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "queryParam=value", "", """{ "message" : "Request url starts with /post url and contains queryParam=value" }""")) allStubs.add(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Request url starts with /post url and body contains bodyParam:value" }""")) return allStubs } fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) } fun addMockStub(stub: RequestClass) { stubsList.add(stub) } }
Для этого я создал открытый класс
RequestClass, все поля которого по умолчанию пустые. Для данного класса я задаю функцию response, которая создает объект MockResponse (возвращающую ответ 200 или некий иной responseText), и функцию matcher, возвращающую true или false.open class RequestClass(val path:String = "", val query: String = "", val body:String = "", val responseText: String = "") { open fun response(code: Int = 200): MockResponse = MockResponse() .setResponseCode(code) .setBody(responseText) open fun matcher(apiCall: String, apiBody: String): Boolean = apiCall.startsWith(path)&&apiCall.contains(query)&&apiBody.contains(body) }
В результате я могу строить более сложные сочетания условий для стабов. Мне эта конструкция показалась более гибкой, хотя принцип в ее основе очень простой.
Но более всего в этом подходе мне понравилось, что я могу подставлять какие-то стабы прямо на ходу, если появилась необходимость что-то поменять в ответе мок-сервера на одном тесте. При тестировании больших проектов такая задача возникает довольно часто, например при проверке каких-то специфических сценариев.
Замену можно осуществить следующим образом:
fun replaceMockStub(stub: RequestClass) { val valuesToRemove = ArrayList<RequestClass>() stubsList.forEach { if (it.path.contains(stub.path)&&it.query.contains(stub.query)&&it.body.contains(stub.body)) valuesToRemove.add(it) } stubsList.removeAll(valuesToRemove) stubsList.add(stub) }
При такой реализации диспатчера тесты остаются простыми. Я также стартую мок-сервер, только выбираю
ListingDispatcher.class ListingDispatcherTest: BaseTest() { @Rule @JvmField var mActivityRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java) private val dispatcher = ListingDispatcher() @Before fun initMockServer() { val mockServer = MockWebServer() val ip = InetAddress.getByName("127.0.0.1") val port = 8080 mockServer.setDispatcher(dispatcher) mockServer.start(ip, port) } . . . }
Ради эксперимента я заменил стаб на POST:
@Test fun postReplacedStubTest() { val params: HashMap<String, String> = hashMapOf("bodyParam" to "value") replacePostStub() sendPostRequest("http://localhost:8080/post", params = params) assertResponseMessage("""{ "message" : "Post request stub has been replaced" }""") }
Для этого вызвал функцию
replacePostStub от обычного dispatcher и добавил новый response.private fun replacePostStub() { dispatcher.replaceMockStub(RequestClass("/post", "", "\"bodyParam\":\"value\"", """{ "message" : "Post request stub has been replaced" }""")) }
В тесте выше я проверяю, что стаб был заменен.
Затем я добавил новый стаб, которого не было в дефолтных.
@Test fun getNewStubTest() { addSomeStub() sendGetRequest("http://localhost:8080/some_specific_url") assertResponseMessage("""{ "message" : "U have got specific message" }""") }
private fun addSomeStub() { dispatcher.addMockStub(RequestClass("/some_specific_url", "", "", """{ "message" : "U have got specific message" }""")) }
Request Verifier
Последний кейс – Request verifier – обеспечивает не мокирование, а проверку отправляемых приложением запросов. Для этого я точно так же стартую мок-сервер, реализовав диспатчер, чтобы приложение возвращало хоть что-то.
При отправке запроса из теста тот приходит в мок-сервер. Через него я могу получить доступ к параметрам запроса, используя
takeRequest().@Test fun requestVerifierTest() { val params: HashMap<String, String> = hashMapOf("bodyKey" to "value") val headers: HashMap<String, String> = hashMapOf("header" to "value") sendPostRequest("http://localhost:8080/post", headers = headers, params = params) val request = mockServer.takeRequest() assertEquals("POST", request.method) assertEquals("value", request.getHeader("header")) assertTrue(request.body.toString().contains("\"bodyKey\":\"value\"")) assertTrue(request.path.startsWith("/post")) }
Выше я показал проверку на простом примере. Точно такой же подход можно использовать для сложных JSON, в том числе для проверки всей структуры запроса (можно сравнивать на уровне JSON или распарсить JSON на объекты и проверить равенство на уровне объектов).
Итоги
В целом инструмент (okhttpmockwebserver) мне понравился, и я использую его на большом проекте. Безусловно, есть некоторые мелочи, которые я хотел бы изменить.
Например, мне не нравится, что приходится стучаться по локальному адресу (localhost:8080 в нашем примере) в конфигах своего приложения; возможно, я еще найду способ все настроить так, чтобы мок-сервер отвечал при попытке отправить запрос на любой адрес.
Также мне не хватает возможности переадресации запросов – когда мок-сервер отправляет запрос дальше, если у него нет для него подходящего стаба. В данном мок-сервере такого подхода нет. Впрочем, до их внедрения и не дошло, поскольку на данный момент в “боевом” проекте не стоит такой задачи.
Автор статьи: Руслан Абдулин
P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на наши страницы в VK, FB или Telegram-канал, чтобы узнавать обо всех наших публикациях и других новостях компании Maxilect.
