
Примечание редактора: сегодня мы слышим от Ахмада Фадли, одного из наших инженеров по Android из группы Experience, о том, как он и его товарищи по команде фронтенд пытались различными способами отделить себя от зависимости серверных API, от имитации ответов этих API в приложении. для создания выделенного внешнего имитационного сервера для подключения приложения, чтобы поддерживать непрерывный и независимый рабочий процесс тестирования приложений с минимальными изменениями в коде приложения или вообще без них.
Команда Experience в Traveloka отвечает за различные продукты для образа жизни в экосистеме Traveloka, такие как Traveloka Xperience и Movies, и за одну из краеугольных групп, продвигающих Traveloka вперед.
Использование API, предоставляемого серверной частью (BE), является общей задачей Front End (FE) как для разработчиков веб-приложений, так и для мобильных приложений. Здесь в Traveloka не исключение. При работе над такой задачей мы бы предпочли идеальную ситуацию, когда все API-интерфейсы постоянно доступны. К сожалению, как бы нам ни нравилась такая благоприятная ситуация, это случается не каждый раз.
В этой статье мы расскажем, как мы минимизировали зависимость нашего API от BE и в итоге создали инструмент, который дает нам гибкость и независимость во время тестирования.
Определение контракта
Прежде всего, все приведенные ниже подходы требуют, чтобы мы установили контракт API до начала любой разработки, который включает в себя конечную точку API, спецификации запроса и ответа. Обе команды FE и BE собираются вместе для обсуждения контракта API, чтобы выработать проект спецификации, чтобы достичь взаимоприемлемого контракта.
После того, как контракт API заблокирован, команды BE и FE могут приступить к параллельной реализации спецификаций в своей соответствующей среде. Когда обе реализации будут выполнены с обеих сторон, можно начинать процесс интеграции.
Но есть проблема.
Представьте себя инженером по мобильному приложению, работающим над функцией внутри продукта Traveloka Xperience со следующим рабочим процессом разработки:
- Рендеринг пользовательского интерфейса ✅
- Сопоставление модели API с моделью представления ✅
- Реализация функциональной логики ✅
- Тестирование 🤔
После выполнения первых трех задач нам необходимо протестировать мобильное приложение, прежде чем передать его команде QA для проверки качества. Но, к сожалению, в этом случае реализация API командой BE еще не готова или промежуточная машина не работает. В любом случае мы заблокированы.
Если мы ничего не предпримем, наши ежедневные стендапы будут получать отчеты о том, что наш прогресс заблокирован, пока BE не предоставит ответ. Итак, как и всем великим инженерам, нам нужно разблокировать себя. Но как?
Автоматическое тестирование
Один из способов - создать тесты для нашего кода и имитировать ответ API. Но такое тестирование работает только до некоторой степени, потому что, несмотря на проверку нашей логики, модульное тестирование не проверяет правильность пользовательского интерфейса. Более того, мы пока не можем передать мобильное приложение команде QA, потому что им нужно будет вручную протестировать пользовательский интерфейс и функциональность. Нам нужно еще кое-что, чтобы дополнить тесты.
Изменение значения во время выполнения

Идея проста. Мы меняем возвращаемое значение API во время выполнения. Мы можем сделать это, установив точку останова непосредственно перед получением ответа API и изменив значение переменной в соответствии с нашими потребностями.
Плюсы ✅
- Быстро сделать для простого изменения значений.
- Само приложение перестраивается при изменении стоимости.
- Чрезвычайно гибкий. Мы можем выбрать, какое поле или конечную точку мы хотим изменить.
Минусы 🔴
- Невозможно использовать, когда API недоступен.
- Из-за больших модификаций придется потрудиться.
- Требуется дополнительный контекст для команды QA, чтобы полностью понять, где устанавливать точки останова.
- Нам нужно сделать поля изменяемыми, чтобы изменять их во время выполнения.
Хотя мы по-прежнему сможем проводить тестирование, учитывая наличие и доступность необходимых API, у этого подхода есть некоторые недостатки. Одна из них заключается в том, что мы не можем редактировать ответ недоступного API, что подводит нас к следующему подходу.
Абстрагирование уровня API
Абстрагирование от уровня API - это то, что мы должны были сделать с самого начала. Мы создаем уровень абстракции, чтобы при необходимости можно было изменить реализацию, например, в случае выполнения модульных тестов.
P.S: Здесь мы создаем экземпляр
ExperienceApiнепосредственно в презентаторе, что нарушает концепцию IoC. Это сделано для простоты. В коде производственного уровня его следует вводить через конструктор (т.е. внедрение зависимостей).
P.P.S: Также, в зависимости от вашей архитектуры, вы можете ввести
ExperienceApiчерез другой уровень (например,Repository), прежде чем обращаться к нему непосредственно вPresenter
Мы можем легко переключаться между макетом (ExperienceApiMock) и реальной реализацией (ExperienceApiImpl) без изменения остальной части кода.
Плюсы ✅
- Можно использовать, даже если API недоступен.
- Легко как для небольших, так и для больших модификаций.
- Нам не нужно делать поля изменяемыми
Минусы 🔴
- QA по-прежнему требует дополнительного контекста, чтобы знать, где изменить код. Более того, QA придется кодировать как на Swift, так и на Kotlin, если они захотят изменить макет как в iOS, так и в Android.
- Частичное издевательство невозможно. Например, мы хотим, чтобы
getProductбыл высмеян, ноgetProductListвместо этого вызывали настоящий API. - Для каждого изменения фиктивного ответа требуется перестройка приложения. Время сборки может быть значительным для большого проекта.
Давайте рассмотрим каждый из этих трех недостатков в той же последовательности:
Решения
1. Вместо того, чтобы создавать ответ из собственного кода, мы читаем из локального JSON. Затем QA может просто отредактировать этот файл JSON, который также можно повторно использовать для Android и iOS.
2. Введите простой флаг для каждой конечной точки API.
ExperienceApiMock служит маршрутизатором, который направляет запросы либо к BE API, либо читает из локального файла JSON, в зависимости от конфигурации.
Вот схема того, как работает этот подход:

3. Это сложно. Для каждого изменения в ExperienceApiMock, такого как изменение содержимого JSON или включение / выключение определенных конечных точек, нам необходимо перестроить приложение, что занимает примерно три минуты времени сборки в случае нашего приложения для Android.
Прочтите Dagger and Multi-module Traveloka Android App о том, как нам удалось сократить время сборки до одной минуты.
Мы выяснили, что если мы хотим избежать перестройки приложения, нам нужно создать макет вне кода приложения, создав макет сервера. Так родился Reproxy.
Создание Reproxy - Мок-сервер

Идея довольно проста. Мы просто перемещаем ExperienceApiMock за пределы приложения и вуаля, мы решаем проблему №3, описанную выше.
Прежде чем создавать Reproxy, мы гарантируем, что все преимущества, которые у нас есть в предыдущих подходах, остались неизменными, в том числе:
- Легко изменить инженером и специалистом по контролю качества.
- Полезный имитационный ответ как для Android, так и для iOS.
- Поддержка частичного издевательства.
Вдобавок к этому мы также установили следующие требования для Reproxy:
- Минимальное или полное отсутствие изменений в коде приложения для переключения между фиктивным и немодельным режимом.
- Легко настроить и запустить.
Движок: Express JS
Основная цель Reproxy - помочь инженерам и команде QA в выполнении задач FE-тестирования без зависимости от BE. Итак, мы запустили Reproxy на нашей локальной машине, работающей на Express JS, в качестве веб-сервера.
Механизм: реакция на основе правил
Хотя мы используем Express JS, мы не хотим создавать логику маршрутизатора на JavaScript (JS) по нескольким причинам:
- Не все знают JS; создание входного барьера для использования инструментов.
- Конфигурацию нелегко передать инженерам.
- Сервер необходимо перезапускать каждый раз, когда мы вносим какие-либо изменения в конфигурацию.
В качестве решения мы решили создать логику роутера в JSON. У нас есть один файл с именем rules.json, в котором есть список правил, и каждое правило, в свою очередь, имеет индивидуальные condition и response. При достижении condition будет возвращен response.
Приведенная выше конфигурация сначала проверит, является ли URL-адрес experience/product. Если да, то он вернет файл experience/product.json. В противном случае он перенаправит запрос на traveloka.com/real-api.
Вот и все. Вся концепция в основном представляет собой серию операторов if-else, набранных в формате JSON.
Мы определили несколько типов условий для общего использования следующим образом:
ANY: Соответствовать чему угодно.ANY_OF: совпадение хотя бы с одним.COMPOSITE_AND: Соответствовать всем условиям.URL_MATCH: точное соответствие URL.URL_PREFIX: соответствие префиксу URL (полезно для URL со строками запроса).
И то же самое для типов ответов, как таковых:
JSON_FILE: читать локальный файл JSON.TEXT: вернуть что-нибудь внутри значения.REDIRECT: перенаправить запрос на указанный URL.DELAY: отложить ответ (лучше всего использовать с SEQUENCE).SEQUENCE: запустить несколько ответов.
Хороший пример использования SEQUENCE и DELAY - дать некоторую задержку перед возвратом определенного значения, чтобы мы могли проверить состояние загрузки на странице приложения:
Теперь нам нужно дать Reproxy понять, как читать эти правила. Здесь нет ничего необычного; просто простой цикл, который ищет первое правило сопоставления, а затем создает ответ на основе этого правила сопоставления:
После того, как все элементы настроены, все, что нам нужно сделать, это перенаправить приложение для вызова Reproxy вместо реального сервера API.
Требуются изменения в коде приложения
С предпочтением минимального изменения кода приложения, как упоминалось ранее, с Reproxy нам нужно изменить только базовый URL.
Вместо того, чтобы вызывать traveloka.com/real-api (не настоящую конечную точку API), мы меняем базовый URL на IP_ADDRESS:3000/run, и все. Не нужно ничего добавлять ни в заголовок, ни в полезные данные запроса.
Этот IP-адрес является общедоступным адресом машины, на которой работает Reproxy. Если вместо этого мы запустим его на нашей локальной машине, чтобы мы могли подключиться с эмулятора / симулятора, то мы можем использовать следующие IP-адреса по умолчанию: 10.0.2.2 для эмулятора Android и 127.0.0.1 для симулятора iOS.
Если мы хотим использовать реальное устройство, нам нужно подключить сервер и устройство в одной сети и использовать локальный IP-адрес сервера.
Советы от профессионалов: в приложении отладки Traveloka, чтобы предотвратить перестройку приложения, мы создали функцию для изменения базового URL-адреса во время выполнения при переключении между Reproxy и любым другим сервером.
Вот и все! Мы выполнили все требования для использования Reproxy:
- Простая конфигурация JSON для различных типов ответов.
- Возможность делать частичное насмешку с помощью
REDIRECT. - В приложении требуется только одно изменение: изменение БАЗОВОГО URL.
- Всего одна командная строка в Терминале для запуска Reproxy (любезно предоставлено Express JS).
Теперь мы можем уверенно разрабатывать и тестировать наши функции, даже если BE еще не завершила реализацию API.
Evolving Reproxy: больше, чем простой сервер-заглушка
Хотя мы достигли всех первоначальных целей создания Reproxy, стали появляться и другие варианты использования по мере того, как все больше людей пробуют инструменты с вопросами или отзывами, такими как:
«Могу ли я различать ответы на основе значений в строках запроса?»
«Мне нужен другой ответ в зависимости от полезной нагрузки запроса».
«Я хотел вызвать настоящий API и ввести несколько новые поля к ответу ».
Может быть очевидно, что нам нужно добавить несколько новых типов условий, например QUERY_STRING_MATCH & REQUEST_MATCH. Но как сделать это масштабируемым? Написание новых типов для каждого нового варианта использования в одном файле JS, несомненно, мгновенно раздувает файл.
Представляем JS_SCRIPT Type
Вместо того, чтобы записывать всю логику в файл responseCreator.js , мы вводим новый тип, который будет выполнять файл JS на основе объявленных аргументов (т. Е. Шаблона делегирования):
Когда это конкретное правило проверяется маршрутизатором, он вызывает queryStringMatch.js и передает что-либо внутри поля аргументов. Создатель сценария решает, как использовать значения этих аргументов.
Далее мы создаем queryStringMatch.js
И последний шаг, мы добавляем новый обработчик типа в наш responseCreator.js
Теперь любой может расширить функциональность инструментов, не изменяя файлы ядра Reproxy.
Лучшая группировка: ответ набора правил
Теперь мы можем предотвратить раздутие responseCreator.js из-за будущих модификаций инструментов. Но мы по-прежнему определяем все правила в одном файле; файл rules.json.
Поскольку мы добавляем новые конфигурации для нескольких продуктов, таких как Flight, Accommodation или Experience, нам нужен надежный способ сделать rules.json файл управляемым.
Введите RULE_SET Тип ответа.
RuleSet - это способ определить набор правил. rules.json - один из примеров RuleSet. С типом RULE_SET мы можем установить RuleSet как response:
И в experienceRules.json:
Наконец, в нашем responseCreator.js файле, когда мы нажимаем тип ответа RULE_SET, он просто снова вызывает ту же функцию, но с новым RuleSet.
Другие варианты использования
Теперь нам удалось создать простой, но мощный инструмент имитации. Он обеспечивает базовую функциональность, как обычный сервер-заглушка. Но он может быть настолько сложным, насколько это необходимо, если использовать тип JS_SCRIPT. Итак, каковы другие варианты использования?
Мок для тестирования автоматизации
Наша команда тестируемого инженера по разработке программного обеспечения (SDET) запускает тесты автоматизации на нашем промежуточном сервере. У них есть несколько болевых точек:
- Промежуточный сервер может неожиданно выйти из строя, что приведет к неверным результатам некоторых тестов.
- Некоторые угловые случаи трудно воспроизвести. Например, результаты поиска, которые возвращают только один товар или информацию о продукте, имеют только одно изображение в фотогалерее.
Reproxy звучит как хорошее решение для устранения упомянутых выше болевых точек. Мы можем просто использовать его как план резервного копирования, когда промежуточный сервер не работает. И для этих крайних случаев мы можем просто создать файл JSON, который удовлетворяет требованиям.
Сейчас мы тестируем этот проект и поделимся результатами в следующей части этой темы о том, что на самом деле сработало бы, а что не сработало бы.
Мок для заинтересованных сторон для тестирования невыпущенной функции
Когда наши менеджеры по продуктам или команда разработчиков хотят протестировать новую функцию, над которой мы работаем, мы можем продемонстрировать им эту функцию, даже если BE еще не готов, развернув Reproxy на машине, доступной из внутренней сети Traveloka.
Но это означает, что мы больше не можем использовать жестко запрограммированный rules.json файл в качестве точки входа, поскольку многие функции разрабатываются одновременно, и для каждого из этих правил требуются разные RuleSet конфигурации.
Наше решение - сделать точку входа RuleSet частью базового URL. Это означает, что вместо вызова IP_ADDRESS:3000/run мы меняем его на IP_ADDRESS:3000/run/{ruleSetName}
С этой новой настройкой, когда кто-то хочет получить ранний доступ к нашей функции, мы можем просто сказать:
«О, просто измените базовый URL-адрес на IP_ADDRESS:3000/run/experienceShinyNewFeature»
Спасибо, что дочитали до конца! Хотя мы являемся разработчиками мобильных приложений, это не означает, что мы должны ограничивать объем нашей работы только разработкой мобильных приложений.
Хорошие инженеры-программисты должны творчески подходить к тому, чтобы освободиться от зависимости. Если это предложение вам очень понравится, возможно, вы получите массу удовольствия, ведь мы вместе с нами создаем мобильное приложение мирового класса! Посетите нашу страницу вакансий или напишите мне сообщение, если вам интересно.