Наверное каждый разработчик и QA-инженер, в рабочем процессе сталкивался с необходимостью подмены отправляемых/принимаемых данных. Когда эта задача касается данных, гуляющих между клиентом и сервером, особых проблем она не приносит. Запрос можно модифицировать и отправить ручками, к примеру через Postman, а для модификации ответа можно использовать инструменты вроде Burp Suite, Charles и т.д., но что делать если целевой запрос отправляется с сервера?

Рассмотрим простую схему процесса оплаты:

  1. Пользователь заполняет платежные данные

  2. Фронт отправляет их на endpoint API

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

  4. Фронт отображает сообщение для клиента

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

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

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

Рассмотрим простую схему процесса бронирования:

  1. Пользователь заполняет данные о пассажирах

  2. Фронт отправляет их на endpoint API

  3. Бэкэнд запрашивает у поставщика актуальную информацию о рейсе

  4. Далее формирует запрос на бронирование с учетом полученной ранее актуальной информации о рейсе и отправляет ответ на фронт

  5. Фронт отображает сообщение для клиента

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

UPD: После пары коротких дискуссий в коментариях, мне показалось что не все читатели "правильно" понимают описываемую мной проблему. Поэтому решил добавить максимально конкретный и однозначный пример ситуации, в которой был бы полезен инструмент, о котором дальше и пойдет речь.

Реальный пример из жизни

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

Цена у поставщика может измениться. Это случай достаточно редкий, но возможный. Поэтому в момент бронирования, мы должны актуализировать данные о билете, чтоб не продать его себе в минус. Если цена в момент актуализации увеличилась, система должна уведомить об этом клиента, а процесс бронирования прерваться, т.к. новая цена его может не устроить. Если же цена уменьшилась, то система может заработать доп. прибыль, ведь клиенту можно и не сообщать об изменении в меньшую сторону, а разницу забрать себе.

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

Допустим программист написал логику, которая реализует все описанные выше требования. Задача уходит на тестирование в QA-отдел. Но т.к. подобная ситуация (изменения цены или условий при актуализации) происходит крайне редко, задача зависает в тестировании надолго. Рано или поздно к разработчику приходит QA-специалист и говорит: "Хоть убей, не могу воспроизвести случай, когда изменятся условия или цена.. Помоги, а?".

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

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

Если вы пишете на PHP и вам знакома эта боль, добро пожаловать под кат.


Дальнейшая часть статьи является мануалом к библиотеке https://github.com/Chetkov/http-client-mitmproxy, которую предлагаю в качестве решения описанной выше проблемы.

Библиотека предоставляет набор инструментов для клиентской и серверной стороны: 

  1. Консольный клиент для пользователя

  2. Декоратор над \Psr\Http\Client\ClientInterface, который через канал коммуникации общается с консольным клиентом и дает возможность модифицировать запросы и ответы в интерактивном режиме

Для наглядности я подготовил demo-проект https://github.com/Chetkov/http-client-mitmproxy-example. Сильно изощряться не стал, т.к. считаю что это было бы избыточно, но для демонстрации работы http-client-mitmproxy его вполне достаточно.

В нем доступны:

  1. artisan команда currency-rates:show {code} (возвращает список курсов валют)

  2. endpoint /currency-rates (возвращает список курсов валют)

  3. endpoint /currency-rates/{code} (возвращает курс конкретной валюты)

  4. endpoint /calculate (делает несколько запросов к currency-rates/{code}, получает курсы и рассчитывает суммы в разных валютах, на основе суммы в рублях)

Установка http-client-mitmproxy

composer require v.chetkov/http-client-mitmproxy

Настройка http-client-mitmproxy

Общее

В данный момент, канал коммуникации \Chetkov\HttpClientMitmproxy\Communication\CommunicationChannelInterface между декоратором и пользовательским клиентом, реализован на базе Redis, поэтому на клиентской и серверной стороне, в конфиге необходимо указать идентичные данные для подключения к нему:

<?php

declare(strict_types=1);

return [
   // For redis based communication channels
   'redis' => [
       'host' => 'redis-host',
       'port' => 6379,
       'timeout' => 0,
   ],
];

Настройка бэкэнда

Также, на стороне бэкэнда, для нужного окружения, в случае обнаружения ProxyUID, в качестве реализации \Psr\Http\Client\ClientInterface необходимо использовать декоратор, поставляемый библиотекой:

<?php 

// ...

$this->app->bind(ClientInterface::class, function (Container $container) {
    $client = new Client(['allow_redirects' => true]);


    if ($proxyUid = ProxyUID::detect()) {
        $config = require dirname(__DIR__, 2) . '/config/mitmproxy.config.php';
        $mitmproxyFactory = new DefaultFactory($config);


        $client = $mitmproxyFactory->createHttpClientDecorator($proxyUid, $client);
    }

    return $client;
});

Использование

Запускаем PHP web-сервер

cd http-client-mitmproxy-example
php -S localhost:8000 -t public/

Запускаем консольный клиент

С помощью опций можно задать:

  • --config путь к файлу конфигурации

  • --temp-dir путь к временной директории (должна быть доступна для записи)

  • --app-mode режим работы целевого приложения (cli, web)

  • --format предпочитаемый формат для редактирования данных (yaml, json, php)

  • --editor предпочитаемый редактор (nano, vim, gedit)

В случае отсутствия в списке опций последних трех (app-mode, format, editor), клиент запросит их в интерактивном режиме

Затем будет выведено сообщение с дальнейшими инструкциями.

Для WEB mode:

Для CLI mode:

Запускаем целевое приложение

Следуя предложенным инструкциям выполним artisan команду

export MITM_PROXY_UID=0ff6b1f2a9ebc702ea9b84b0fe019f6b &&
php artisan currency-rates:show USD

И подменим дату в отправляемом к API центробанка запросе на 2010 год

Затем согласимся с редактированием полученного от центробанка ответа

И изменим название доллара США на "Зелёный"

Затем откажемся от продолжения редактирования других полей и всего ответа целиком и увидим сообщение о завершении текущей сессии

Теперь клиент ожидает новых соединений (т.е. процессов, запущенных с его ProxyUID), а запущенная artisan команда думает, что USD на сайте центробанка называется “Зелёный” и его актуальный курс “30”

Думаю, на этом статью можно закончить. Спасибо за внимание.

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

UPD 2: Огромная просьба к читателям, оставляющим свой голос в опросе - пожалуйста, аргументируйте ваш выбор хотя-бы коротким коментарием. Особенно, если вы считаете инструмент бесполезным, расскажите почему. Еще не сталкивались с подобной проблемой? Сталкивались, но решали её подругому?

Комментарии (13)


  1. vchetkov Автор
    00.00.0000 00:00

    Сейчас попробую описать более конкретный пример.

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

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

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

    Мы написали логику, которая реализует все описанные выше требования. Задача уходит на тестирование в QA-отдел. Но т.к. подобная ситуация происходит крайне редко, задача зависает в тестировании на долго. Рано или поздно к разрабочику приходит QA-специалист и говорит: "Хоть убей, не могу воспроизвести случай, когда изменятся условия.. Помоги, а?".

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

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


  1. Ksoo
    00.00.0000 00:00

    Идея подменять что то в запросах ходящих в проде плохая и опасная.

    Логи и сквозной requestId должны давать возможность понимания что пошло не так.


    1. vchetkov Автор
      00.00.0000 00:00

      Так речь и не о проде)

      Логи и сквозной requestId должны давать возможность понимания что пошло не так.

      Давать возможность понимания, и давать возможность повторения крайне редко возникающей ситуации - разные вещи


      Смысл в предоставлении возможности интерактивной модификации запросов/ответов, ходящих не от клиента к серверу, а между сервером и другими системами, на тех окружениях, на которых разработчик сочтет это нужным (stage, dev, test - решает уже разработчик, а статья описывает возможности инструмента)


      1. Ksoo
        00.00.0000 00:00

        Если не прод, то тесты на контракты каждого сервиса будут эффективней


        1. vchetkov Автор
          00.00.0000 00:00
          +1

          Мы видимо говорим о разных проблемах.

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

          • Проблема может возникать не при взаимодействии внутренних сервисов, а при взаимодействии сервиса с внешней системой.


  1. anonymous
    00.00.0000 00:00

    НЛО прилетело и опубликовало эту надпись здесь


    1. vchetkov Автор
      00.00.0000 00:00

      Т.е. для того, что бы найти багу в уровне приложения

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


    1. vchetkov Автор
      00.00.0000 00:00

      Сейчас попробую описать более конкретный пример.

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

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

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

      Мы написали логику, которая реализует все описанные выше требования. Задача уходит на тестирование в QA-отдел. Но т.к. подобная ситуация происходит крайне редко, задача зависает в тестировании на долго. Рано или поздно к разрабочику приходит QA-специалист и говорит: "Хоть убей, не могу воспроизвести случай, когда изменятся условия.. Помоги, а?".

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

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


  1. GadenbIsh
    00.00.0000 00:00

    А почему бы не реализовать прокси для api поставщика, которое будет при необходимости что-то подменять и включать это прокси в конфиге приложения для тестирования по необходимости?


    1. vchetkov Автор
      00.00.0000 00:00

      А можешь чуть детальнее раскрыть свою идею?


      1. GadenbIsh
        00.00.0000 00:00

        есть апи поставщика http://provider.com/api который возвращает json, с которым работает приложение

        есть конфиг приложения, который использует это апи:
        $apiUrl = "http://provider.com/api"

        Реализовываем прокси http://provider.local задачи которого

        1. получить запрос к поставщику, как пришел от приложения

        2. отправить его в http://provider.com/api

        3. получить ответ от поставщика http://provider.com/api

        4. отдать ответ приложению

        меняем конфиг тестового окружения приложения на:
        $apiUrl = "http://provider.local"

        теперь между пунктами 1 и 2, а также 3, 4 - делаем какие угодно преобразования запроса/ответа (внутри прокси provider.local, надо лишь реализовать интерфейс, который будет делать подмену значений)

        допустим, в json ответе заменяем "доллар" на "зеленый":
        в пункте 3 пришел доллар, делаем замену на зеленый, отдаем приложению модифицированный ответ


        1. vchetkov Автор
          00.00.0000 00:00

          А если поставщиков 2, 3 ... 5? Это ведь все сторонние системы, с разными API. Имхо, идти по этому пути и под каждую систему писать новый proxy API - замахаешься.

          К тому же, в твоем варианте решения этой проблемы, отсутствуют самые ключевые черты, которых я хотел добиться, а именно:

          1. Полная независимость QA от отдела разработки.
          QA сам новый прокси не напишет, на реализацию каждого такого прокси API нужно привлечь разработчика.

          2. Ускорение процесса тестирования.
          Если каждый раз, когда нам нужно будет проверить какой-то редкий кейс, для этого нужно будет писать целый сервис, обеспечивающий подмену данных, реализующий даже небольшую часть API поставщика, то ИМХО, проще уже обойтись костылями о которых я писал в статье, это будет гораздо проще и быстрее.

          3. Возможность интерактивной модификации данных.
          На самом деле, я рассматривал варианты мока API, перед тем как делать этот инструмент. Поверхностно ковырял такие штуки как mock-servers в Postman и Hoverfly, но того чего хотел, я там не нашел.

          Чтоб воспроизвести какой-то кейс - нужно сначала подменить подключения к API, сделать запрос, записать дамп ответа, потом отредактировать его нужным образом и настроить mock метод API. Много рутины, не удобно, не гибко, и сложность настройки возрастает кратно росту сложности самого воспроизводимого кейса.

          С помощью предлагаемого мной инструмента, достаточно 1 раз настроить на нужном окружении бэкэнд. После этого QA может отслеживать все отправляемые бэкэндом запросы, налету подменять их данные и данные ответов на них. Все это происходит в реал-тайм, без сложной конфигурации всяких-там mock, proxy серверов и т.д.

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

          С http-client-mitmproxy этих проблем не возникает.


          1. GadenbIsh
            00.00.0000 00:00

            Я всего лишь описал идею. А дальше уже вопрос реализации и масштабирования.

            Например, пишем абстрактный прокси, который по домену или урлу понимает, куда надо переслать данные:
            http://proxy.local/provider1.com/
            http://proxy.local/provider2.com/
            и прописываем в конфигах приложения их.

            Делаем интерфейс для QA, который содержит два инпута: что на что заменить.

            Можно перед выполнением запроса подготовить маппинг "доллар->зеленый", можно заставить приложение ожидать и вообще в реалтайме менять ответ от поставщика.

            1. Прокси пишет разработчик и дает qa интерфейс.

            2. Не нужно реализовывать "небольшую часть API поставщика", proxy может передавать данные в точности так, как передает приложение. Банально regexpом делать замену одних данных на другие в ответе поставщика. Прокси не нужно понимать, как работать с конкретным поставщиком, чтобы просто передать данные дальше.

            3. Прокси позвоялет интерактивно менять данные (вопрос будет только в таймауте приложения): получили ответ от поставщика, дали интерфейс для QA, чтобы посомтреть на него, поменять, послать в приложение. Для приложения это будет просто тупящий запрос.

            Главный минус твоего подхода - нужно модифицировать исходный код приложения. Вставлять такой код опасно, даже подразумевая, что он на прод не попадет/работать там не будет. Это потенциальная дыра.


            1. vchetkov Автор
              00.00.0000 00:00

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

              Вставлять такой код опасно, даже подразумевая, что он на прод не попадет/работать там не будет. Это потенциальная дыра.

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

              В общем, вашу идею я понял, еще раз спасибо за дискуссию. Этот разговор подкинул мне парочку интересных мыслей)