Разработка мобильного приложения без сервера

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

    Такая ситуация может происходить по разным причинам. Однако, чаще всего на старте разработки, бэкэнд просто не написан и клиент начинает без него. В таком случае начало разработки затягивается на 2-4 месяца

    Иногда сервер просто отключился (упал), иногда не успевает выкатывать нужные методы, иногда есть проблемы с данными и т.п. Все эти проблемы привели нас к написанию небольшого сервиса Mocker, который позволяет подменить реальный бэкэнд.



    Как я к этому пришел
    Как я вообще к этому пришел? Заканчивался мой первый год работы в компании и меня поставили на новенький e-commerce проект. Менеджер сказал, что проект нужно сделать за 4 месяца, но бэкэнд команда (на стороне заказчика) начнет разработку только через 1.5 месяца. А мы за это время должны накидать уже много UI-фич.

    Я предложил написать моковый бэкэнд (до того как стать iOS разработчиком я игрался с .NET в универе). Идея реализации была проста: нужно было по заданной спецификации написать методы-заглушки, которые бы брали данные из заранее подготовленных JSON-файлов. На том и порешили.

    Через 2 недели я ушел в отпуск и задумался: «А чего бы мне не генерировать все это автоматически?». Так за 2 недели отпуска я написал подобие интерпретатора, который берет спецификацию APIBlueprint и генерит из нее .NET Web App (код на C#).

    В итоге появилась первая версия этой штуки и мы жили на ней почти 2.5 месяца. Я не могу привести реальных цифр, насколько это нам помогло, но помню, как на ретроспективе говорили, что если бы не эта система, никакого релиза не было бы.

    Сейчас, спустя несколько лет, я учел допущенные мной ошибки (а их было очень много) и полностью переписал инструмент.

    Пользуясь случаем — большое спасибо коллегам, которые помогали обратной связью и советами. А также руководителям, которые терпели весь мой «инженерный произвол».

    Введение


    Как правило, любое клиент-серверное приложение выглядит примерно вот так:



    На каждый экран приходится как минимум 1 запрос (а часто больше). Переходя по экранам вглубь, нам нужно сделать все больше и больше запросов. Иногда мы даже не можем сделать переход, до тех пор пока сервер не скажет нам «Покажи кнопку». То есть мобильное приложение очень сильно завязано на сервер, не только во время своей непосредственной работы, но и на этапе разработки. Рассмотрим абстрактный цикл разработки продукта:



    1. Сначала мы проектируем. Декомпозируем, описываем и обсуждаем.
    2. Получив задачи и требования, начинаем разработку. Пишем код, верстаем и т.п.
    3. После того, как мы что-то реализовали, готовится сборка, которая уходит на ручное тестирование, где работа приложения проверяется по разным кейсам.
    4. Если у нас все нормально, и тестеры апрувят сборку, она уходит заказчику, который выполняет приемку.

    Каждый из этих процессов очень важен. Особенно последний, так как заказчик должен понимать на каком этапе мы действительно находимся, а иногда ему нужно отчитываться о результатах перед руководством или инвесторами. Как правило, подобные отчеты происходят, в том числе, в формате демонстрации мобильного приложения. На моей практике был случай, когда заказчик демонстрировал буквально половину MVP, которая работала только на моках. Приложение на моках выглядит как настоящее и крякает как настоящее. Значит оно настоящее (:
    Однако это розовая мечта. Давайте рассмотрим, что произойдет на самом деле, если у нас не будет сервера.



    1. Процесс разработки будет проходить медленнее и болезненнее, так как сервисы мы написать нормально не можем, проверить все кейсы тоже не можем, приходится писать заглушки, которые потом нужно будет удалить.
    2. После того, как мы с горем пополам сделали сборку, она попадает тестерам, которые смотрят на нее и не понимают что с ней делать. Проверить ничего нельзя, половина вообще не работает, потому что сервера нет. Как следствие — пропускают много багов: как логических, так и визуальных.
    3. Ну а после «как смогли посмотрели», надо отдать сборку заказчику и тут начинается самое неприятное. Заказчик не может толком оценить работу, он видит 1-2 кейса из всех возможных и уж точно не может показать это своим инвесторам.

    В общем, все идет по наклонной. И к сожалению, такие ситуации случаются почти всегда: иногда сервера нет пару месяцев, иногда пол года, иногда просто в процессе сервер сильно опаздывает или необходимо быстро проверить граничные случаи, которые воспроизвести с помощью манипуляций данными на реальном сервере практически невозможно.

    Например, мы хотим проверить как поведет себя приложение если у пользователя платеж проходит дольше положенного срока. Воспроизвести такую ситуацию на сервере очень сложно (и долго) и нам надо делать это искусственно.

    Таким образом, есть следующие проблемы:

    1. Сервер отсутствует полностью. Из-за этого невозможно разрабатывать, проверять и презентовать.
    2. Сервер не успевает, что мешает разрабатывать и может мешать тестировать.
    3. Мы хотим тестировать граничные кейсы, а сервер не может этого позволить без долгих телодвижений.
    4. Аффектит тестирование и угрожает презентации.
    5. Сервер падает (однажды мы уже во время стабильной разработки лишились сервера на 3.5 дня).

    Чтобы бороться с этими проблемами и был создан Mocker.

    Принцип работы


    Mocker — небольшой веб-сервис, который где-то хостится, слушает трафик на каком-то определенном порту и умеет отвечать заранее заготовленными данными на конкретные сетевые запросы.

    Последовательность следующая:

    1. Клиент отправляет запрос.
    2. Mocker получает запрос.
    3. Mocker находит нужный мок.
    4. Mocker возвращает нужный мок.

    Если с пунктами 1,2 и 4 все понятно, то 3 вызывает вопросы.

    Для того, чтобы понять, как сервис находит нужный клиенту мок, сначала рассмотрим структуру самого мока.

    Мок — это файл с JSON-ом в следующем формате:

    {
        "url": "string",
        "method": "string",
        "statusCode": "number",
        "response": "object",
        "request": "object"
    }
    

    Разберем каждое поле отдельно.

    url


    Этот параметр используется для того, чтобы указать URL запроса, по которому обращается клиент.

    Например, если мобильное приложение делает запрос на url host.dom/path/to/endpoint, то в поле url нам нужно написать /path/to/endpoint.
    То есть это поле хранит относительный путь до эндпоинта.

    Это поле должно быть отформатировано в формате url-template, то есть допускается использовать следующие форматы:

    1. /path/to/endpoint — обычный url адрес. Во время получения запроса сервис будет сравнивать строки посимвольно.
    2. /path/to/endpoint/{number} — url с path-паттерном. Мок с таким URL будет реагировать на любой запрос, который удовлетворяет этому шаблону.
    3. /path/to/endpoint/data?param={value} — url c parameter-паттерном. Мок с таким url сработает на запрос, содержащий заданные параметры. При этом, если одного из параметров не будет в запросе, то он не будет соответствовать шаблону.

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

    method


    Это ожидаемый http method. Например POST или GET.
    Строка обязательно должна содержать только заглавные буквы.

    statusCode


    Это код http статуса для ответа. То есть запросив этот мок, клиент получит ответ со статусом записанным в поле statusCode.

    response


    Это поле содержит JSON объект, который будет отправлен клиенту в теле ответа на его запрос.

    request


    Здесь записывается тело запроса, которые ожидается получить от клиента.Это будет использоваться для того, чтобы отдать нужный response в зависимости от тела запроса request. Например, если мы хотим менять ответы в зависимости от параметров запроса.

    {
        "url": "/auth",
        "method": "POST",
        "statusCode": 200,
        "response": {
            "token": "cbshbg52rebfzdghj123dsfsfasd"
        },
        "request": {
            "login": "Tester",
            "password": "Valid"
        }
    }
    

    {
        "url": "/auth",
        "method": "POST",
        "statusCode": 400,
        "response": {
            "message": "Bad credentials"
        },
        "request": {
            "login": "Tester",
            "password": "Invalid"
        }
    }
    

    Если клиент отправит запрос с телом:

    {
        "login": "Tester",
        "password": "Valid"
    }
    

    То в ответ он получит:

    {
        "token": "cbshbg52rebfzdghj123dsfsfasd"
    }
    

    А в случае, если мы хотим проверить, как будет работать приложение если пароль введен неверно, то отправится запрос с телом:

    {
        "login": "Tester",
        "password": "Invalid"
    }
    

    То в ответ он получит:
    {
        "message": "Bad credentials"
    }
    

    И мы сможем проверить кейс с неверным паролем. И так для всех остальных кейсов.

    А теперь разберемся как работает группировка и поиск нужного мока.



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

    Сервер объединяет разные моки по url и method. Это необходимо в том числе и для того, чтобы мы могли создать на один url много разных моков.

    Например, мы хотим, чтобы постоянно дергая Pull-To-Refresh, приходили разные ответы и состояние экрана все время менялось (чтобы проверить все граничные кейсы).

    Тогда мы можем создать много разных моков с одинаковыми параметрами method и url, а сервер будет возвращать их нам итеративно (по очереди).
    Например, пусть у нас будут такие моки:

    {
        "url": "/products",
        "method": "GET",
        "statusCode": 200,
        "response": {
            "name": "product",
            "currency": 1,
            "value": 20
        }
    }
    

    {
        "url": "/products",
        "method": "GET",
        "statusCode": 200,
        "response": {
            "name": "gdshfjshhkfhsdgfhshdjgfhjkshdjkfsfgbjsfgskdf",
            "currency": 5,
            "value": 100000000000
        }
    }
    

    {
        "url": "/products",
        "method": "GET",
        "statusCode": 200,
        "response": null
    }
    

    {
        "url": "/products",
        "method": "GET",
        "statusCode": 400,
        "response": null
    }
    

    Тогда, когда мы первый раз вызовем метод GET /products, то сначала получим в ответ:

    {
        "name": "product",
        "currency": 1,
        "value": 20
    }
    

    Когда вызовем второй раз — указатель итератора сместится на следующий элемент и нам вернется:

    {
        "name": "gdshfjshhkfhsdgfhshdjgfhjkshdjkfsfgbjsfgskdf",
        "currency": 5,
        "value": 100000000000
    }
    

    И мы сможем проверить как поведет себя приложение если мы получим какие-то большие значения. И так далее.

    Ну, а когда мы дойдем до последнего элемента и еще раз вызовем метод, то нам вернется снова первый элемент, потому что итератор возвратится к первому элементу.

    Кэширующий прокси


    Mocker умеет работать в режиме кэширующего прокси. Это означает, что когда сервис получает запрос от клиента, он достает из него адрес хоста, на котором расположен реальный сервер и схему (для определения протокола). Далее берет полученный запрос (со всеми его хедерами, так что если метод требует аутентификации, то ничего страшного, ваш Authorization: Bearer ... перенесется) и вырезает из него служебную информацию (тот самый host и scheme) и отправляет запрос на реальный сервер.

    Получив ответ с 200-м кодом Mocker сохраняет ответ в моковый файл (да, вы потом можете его скопировать или поменять) и возвращает клиенту то, что он получил от реального сервера. Причем, он не просто сохраняет файл в случайное место, а организует файлы так, чтобы с ними можно было затем работать вручную Например, Mocker отправляет запрос по следующему URL: hostname.dom/main/products/loans/info. Тогда он создаст папку hostname.dom, затем внутри нее он создаст папку main, внутри нее папку products

    Чтобы моки не дублировались, название формируется на основе http-метода (GET, PUT...) и хеша от тела ответа реального сервера. В таком случае, если на конкретный ответ уже существует мок, то он просто перезапишется.

    Эту фичу можно активировать индивидуально для каждого запроса. Для этого нужно добавить три хедера к запросу:

    X-Mocker-Redirect-Is-On: "true",
    X-Mocker-Redirect-Host: "hostaname.ex:1234",
    X-Mocker-Redirect-Scheme: "http"
    

    Явное указание пути к мокам


    Иногда хочется, чтобы Mocker возвращал только те моки, которые мы хотим, а не все которые есть в проекте.

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

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

    X-Mocker-Specific-Path: path
    

    К примеру, пусть у Mocker-а в корне вот такая структура папок

    root/
        block_card_test_case/
            mocks....
        main_test_case/
            blocked_test_case/
            mocks...
    

    Если необходимо прогнать тест-кейс о заблокированных картах, тогда
    X-Mocker-Specific-Path: block_card_test_case
    Если необходимо прогнать тест-кейс связанный с блокировкой главного экрана, тогда
    X-Mocker-Specific-Path: main_test_case/blocked_test_case

    Интерфейс


    Сначала мы работали с моками напрямую по по ssh, но с ростом числа моков и пользователей перешли на более удобный вариант. Сейчас мы используем CloudCommander.
    В примере docker-compose, он связывается с контейнером Mocker-а.

    Выглядит это примерно так:



    Ну и бонусом идет web-редактор, который позволяет добавлять/изменять моки прямо из браузера.



    Это также временное решение. В в планах уйти от работы с моками через файловую систему к какой-нибудь базе данных. И соответственно, управлять самими моками можно будет из GUI к этой DB.

    Развертывание


    Для того, чтобы развернуть Mocker проще всего использовать Docker. К тому же, развернув сервис из докера, автоматически развернется web-интерфейс через который удобнее работать с моками. Файлы необходимые для развертывания через Docker лежат в репозитории.

    Однако, если вас не устраивает этот вариант, можете самостоятельно собрать сервис из исходников. Для этого достаточно:

    git clone https://github.com/LastSprint/mocker.git
    cd mocker
    go build .
    

    Затем нужно написать конфиг файл (пример) и запустить сервис:

    mocker config.json
    

    Известные проблемы


    • После каждого нового файла надо делать curl mockerhost.dom/update_models для того, чтобы сервис прочел файлы заново. Я не нашел быстрый и элегантный способ обновлять его иначе
    • Иногда CloudCommander багует (или я что-то не так сделал) и он не дает редактировать моки, которые были созданы через web-интерфейс. Лечится чисткой кэша у браузера.
    • Сервис работает только с application/json. В планах поддержка form-url-encoding.

    Итог


    Mocker— это web-сервис, который решает проблемы разработки клиент-серверных приложений в том случае, когда сервер по каким-то причинам не готов.

    Сервис позволяет создавать множество разных моков на один URL, позволяет связать между собой Request и Response с помощью явного указания параметров в url, либо прямо с помощью задания ожидаемого тела запроса. У сервиса есть web-интерфейс, который сильно упрощает жизнь пользователям.

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

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

    Репозиторий на GitHub.
    Surf
    33,09
    Компания
    Поделиться публикацией

    Похожие публикации

    Комментарии 15

      0

      Postman можно вместо этого использовать?

        0
        Не совсем. Postman это конечно одна из альтернатив, но, на сколько я знаю, у него во-первых нет функциональности, которая есть у Mocker, во-вторых (опять же на сколько мне известно) Postman Mock Server в бесплатной версии ограничен по лимитам.

        Этот сервис (Mocker) все таки чем-то так или иначе отличается от своих альтернатив
        0
        Чем это отличается от faker для генерации рандомных данных и json-server в качестве серевера?
          0

          Ну к faker ни как не относится. Если я правильно понял зачем он нужен. А вот json-server это аналог да, но опять же. Вопрос в функциональности. json-server (на сколько мне известно) не позволяет проксировать запрос и сохранять ответ (история про кеширующий прокси), а у этого есть интересный юзкейс — поддержка. Мы используем Mocker как страховочный сервис даже после того, как реальны бек уже есть (дорабатывается). Проксирование позволяет без танцев с бубном поддерживать моки в актуальном состоянии и при этом они структурированно лежат.
          Плюс фича с явным указанием пути. Полезно для тестеров, когда нужно протестировать какой-то конкретный кейс или флоу. Таким способом можно как бы "выделить" что-то вроде окружения в котором работа QA не аффектит всех остальных.
          Ну и я не знаю как будет вести себя json-server если мы создадим 10 разных моков на один запрос. Будет ли он возвращать их итеративно или это вообще не будет работать (это не пагинация). И может ли он вернуть определенный ответ в зависимости от параметров запроса или тела запроса.
          P.S. Спасибо за faker выглядит так, будто его можно прикрутить ко всему этому делу (правда потребность получить кучу случайных данных не часто возникает, но все же)

          0

          У нас упал сервер, так давайте же… напишем свой сервер с блекджеком и!..
          К слову, не очень понятно что мешает на самом дев сервере выдавать фейковые данные пока идёт разработка. Делаем так и все довольны.

            0
            У нас упал сервер, так давайте же… напишем свой сервер с блекджеком и!..

            Ну ведь делалось это не совсем так, да и это далеко не полноценный сервер)


            К слову, не очень понятно что мешает на самом...

            Дело же не только в том, чтобы просто замокать. Есть еще интерактивная часть, когда ты меняешь что-то под себя. На моем опыте бэк не сильно-то желал давать кому-то доступы к тем же мокам, а тем более пилить чтение из файлов, чтоб потом его выпилить (может специфично для аутсорса).
            К тому же иногда бэка как бы просто нет. Он не просто долго пишется — его нет (пока еще).
            Или когда сервер упал. Были кейсы когда уже все выровнялось, но сервак лежал по 3-5 дней (здесь уже моками на дев-сервере не отделаешься). Ну и решение такое почти неинвазивно.

            0
            Честно, такое впечатление, что Вы решили свою заглушку докрутить до уровня конструктора бакендов ;)

            По моему мнению достаточно тестовых данных в объеме для решения двух задач.
            1) Эмуляция сервера
            2) Покрытие тестами кода при разработке бакенда.

            Т.е. эмулятор достаточно научить понимать данные в тех форматах в которых они будут лежать в юнит тестах у разработчиков бакенда.

            И деплоить их на тестовый сервер автоматом из общей репы для фронта и бека

            в unit тестах
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Честно, такое впечатление, что Вы решили свою заглушку докрутить до уровня конструктора бакендов ;)

                Как-то даже не думал об этом)


                По моему мнению достаточно тестовых данных в объеме для решения двух задач.

                Мне кажется это решает только часть проблем при разработке фронта, разве нет? Проблема-то в том, чтобы мобильщик/QA мог что-то там под себя докручивать, что-то по ходу менять. Ну то есть еще очень ценна интерактивность. Ну и в процессы эту штуку можно вписать так, чтобы получить с нее профит при возникновении каких-то форс-мажоров, при ручном тестировании.


                Т.е. эмулятор достаточно научить понимать данные...

                Мне кажется, что не стоит мешать данные из unit тестов бэка и данные, которые использует для себя фронт. Хотя, возможно, я просто Вас не понял)

                0
                Статья конечно интересная, но вызывает вопрос подход:
                На каждый экран приходится как минимум 1 запрос (а часто больше).

                Если нужно написать UI, а серверная часть еще не готова, не лучше ли сделать цепочку
                View-Protocol-Service
                После чего просто сделать фейковую реализацию протокола для сервиса. При желании еще тестов добавить с этой фейковой реализацией.
                Следующим этапом к слову можно сделать
                Service-Protocol-Entity(Network data source)
                Так же с фейковой реализацией и тестами.
                  0

                  Смотрите, если делать реализацию на клиенте, то есть несколько проблем:


                  • Из-коробки нет способа менять ответы на какие хочется прямо в процессе работы. Это можно сделать, но:
                    • Нужно писать инфраструктуру под это (в приложении)
                    • Нужно переключаться между экранами приложения
                    • Не понятно как это шарить нормально. Разве что иметь общий репозиторий из которого это пулится (моки еще другой команде нужны). Выглядит немного "гвоздями прибито" да и консистентность может нарушаться.
                    • Такое же решение нужно реализовывать для всех используемых платформ (iOS, Android, возможно Web)
                    • Редактировать моки из приложения (да и с телефона вообще) не удобно.
                  • Появляется инвазивность. То есть нам нужно будет делать какие-то потуги, чтобы одно переключить на другое. (сейчас к примеру мы просто один URL на другой заменяем флагами)
                  • Возможность хранить тест-кейсы (как моки) отдельной стройной конструкций (и не трогать разработчиков для их поддержки) тоже пропадает.

                  Здесь идея как раз в том, чтобы разработчики клиентской части вообще не заморачивались, а писали как пишут. Просто подменяется "реализация" сервера. Поэтому все то, что тестеры обычно проверяют (что будет если запрос обвалится по таймауту, что будет при долгом запросе и т.п.) остается неизменным.

                    0
                    Идея понятна, ее я одобряю.
                    Не понятен именно подход делать запрос прямо с экрана.
                    Ок, запросы вы вынесли, но ведь UI может еще строится на основании БД, UserDefaults и т.д.
                    Не лучше ли вынести все это в модель, и мокать ее (как вариант на основании все того же тестового сервера). А так как модель связана по интерфейсу, то подменять его реализацию можно все тем же одним флагом
                      0

                      Ааа, вот Вы о чем))
                      Нет, мы не делаем запросы с экрана. У нас нормальная архитектура с протоколами, сервисами и презентерами (кстати тоже в опенсорсе).
                      Я просто не стал усложнять описание, потому что то как описано в статье нагляднее и понятнее (если вдруг ее прочтет не мобильный разработчик)

                        0
                        Тогда следующий вопрос)
                        Не пробовали реально мокать сервис целиком, а не только сетевой слой?
                          0

                          Пробовали, для Unit-тестов.

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

                Самое читаемое