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

Даже если вам повезло и ваш продукт имеет минимум зависимостей от внешних сервисов, скорее всего внутри он разбит на компоненты (классика жанра — backend/frontend), которые можно и нужно тестировать по отдельности. Это значит, что внешней зависимостью уже является api соседнего компонента, команда разработки которого совсем не горит желанием предоставлять вам инструменты для управления его состоянием.

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

Решить эту проблему может мокирование API внешних систем.

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

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

Возможности mountebank'а:

  • мокирование API на протоколах tcp, http, https, smtp;
  • мокирование неограниченного количества API одновременно;
  • гибкое переопределение логики mock-API прямо во время тестов используя конфигурационный API mountebank'a;
  • проксирование запросов в API внешнего сервиса, сохранение ответов и возможность их последующего использования в mock-API;
  • клиентские библиотеки для большинства языков программирования (JS, Python, Ruby, Java и многоие другие).

Концепция


Идея, лежащая в основе mountebank проста и гениальна: не нужно писать один универсальный mock-API для всех тест-кейсов сразу, эффективнее переопределять поведение mock-API перед каждым тестом ровно в том объеме, в каком это требуется конкретному тест-кейсу.

Mountebank представляет из себя веб-сервис который разворачивается на вашей тестовой площадке и который имеет свой web API. Назовём этот API конфигурационным. Конфигурационный API служит для того, чтобы рассказать mountebank'у о том, какую логику вы хотите заложить в свой mock-API. После этого mountebank поднимает imposter – веб-сервис, который и реализует ваш mock-API. При этом mountebank не ограничивает вас в том, насколько часто вы будете переопределять поведение imposter’a.

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

Установка и первый мокированный метод


Знакомство с mountebank’ом следует начинать с установки. Поскольку это node.js приложение, логичнее всего это сделать через npm (требуется предварительно установленный node.js):

>npm install -g mountebank

Если node.js отдельно ставить не хочется, есть и альтернативные пути для различных платформ.
Далее запускаем mountebank из консоли:

>mb

По умолчанию mountebank занимает порт 2525 и если всё хорошо, то видим приветствие:

image

Именно на localhost:2525 сейчас висит конфигурационный API самого mountebank’a.

Для тренировки создадим простенький imposter, который бы реализовывал метод POST /test и если в теле запроса присутствует JSON с объектом

{
    “message”: “ping”
}

то в ответ должен отправляться JSON:

{
    “message”: “pong”
}

Во всех остальных случаях мы хотим чтобы нам возвращался 400 статус (Bad Request).

Общаться с mountebank’ом и с imposter’ами будем через Postman.

Для создания нашего imposter’а отправляем на конфигурационный API mountebank’а следующий запрос:



Пару слов о том, из чего состоит запрос на создание imposter’a.

Вначале мы указываем протокол, по которому будет работать imposter и порт, который он будет слушать. Это и будет порт, на котором поднимется наш mock-API. Далее идет массив stubs: правила для imposter’a как реагировать на тот или иной входящий запрос.

Каждое правило состоит из predicates и responses:

  • predicates описывает параметры, которым должен соответствовать входящий запрос, чтобы imposter на него среагировал
  • responses описывает ответ, который будет отправлен imposter’ом, в случае если поступивший запрос соответствует predicate’ам.

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

Теперь когда мы знаем как устроен imposter пришло время его проверить.

Отправляем imposter’у запрос, соответствующий первому правилу:



И получаем ответ:



Теперь попробуем отправить запрос не соответствующий первому правилу, а значит подпадающий под второе:



В ответ ожидаемо получаем 400 Bad Request:



Всё работает!

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

В то же время mountebank, может держать одновременно поднятыми множество imposter’ов на различных портах и протоколах, это дает возможность мокировать сразу несколько внешних API.

Применение в автотестах


Теперь, когда механика работы mountebank’a стала более-менее понятна попробуем применить его там, где заложенная в него концепция раскроется в полной мере: автотесты.

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

Тест данной функции подразумевает два сценария:

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

В обоих кейсах тестируемая страница отправляет в backend один и тот же запрос: GET /api/points.

Без мокирования API backend’а проверить оба кейса вообще невозможно: backend в обоих случаях будет возвращать одинаковый набор точек не удовлетворяющий ни одному из кейсов.
Даже если мы напишем свой сервис, который будет отдавать определенный набор данных, это позволит нам проверять в автоматическом режиме только один из кейсов.

А теперь посмотрим, как эти тест-кейсы можно автоматизировать с применением mountebank’a.
Для демонстрации я воспользуюсь связкой python + pytest (тестовый фреймворк с мощной подсистемой фикстур).

import pytest
import requests
import json

from page_objects import MapPage

# точки с валидными координатами
valid_points = {
        "point_1": (55.999653, 37.206002),
        "point_2": (55.996767, 37.184545),
        "point_3": (55.984932, 37.208749),
    }

# только третья точка имеет валидные координаты
invalid_points = {
        "point_1": (255.999653, 37.206002),
        "point_2": (55.996767, 237.184545),
        "point_3": (55.984932, 37.208749),
    }

# параметризованная фикстура
@pytest.fixture(
    scope='module',
    params=[valid_points, invalid_points],
    ids=['Valid points case', 'Invalid points case']
)
def fxtr_map(request):
    points = request.param

    # формируем конфигурацию imposter'a
    imposter_cfg = {
        "port": 1987,
        "protocol": "http",
        "stubs": [
            {
                "predicates": [
                    {
                        "equals": {
                            "method": "GET",
                            "path": "/api/points"
                        }
                    }
                ],
                "responses": [
                    {
                        "is": {
                            "statusCode": 200,
                            "headers": {"Content-Type": "application/json"},
                            "body": points
                        }
                    }
                ]
            }
        ]
    }

    # отправляем в mountebank запрос на создание imposter'a
    requests.request('POST',
                     'http://localhost:2525/imposters',
                     data=json.dumps(imposter_cfg),
                     headers={"content-type": "application/json"})

    # запрашиваем страницу с тестируемой функциональностью (карта банкоматов)
    browser = webdriver.Chrome()
    browser.implicitly_wait(10)
    browser.get(MAP_PAGE_URL)
    map_page = MapPage(browser)

    # отбираем точки которые должны отображаться на карте
    points_to_check = {k: points[k] for k in points if points[k] == valid_points[k]}

    # передаем в тест экземпляр тестируемой страницы
    #и точки которые должны отобразиться
    yield map_page, points_to_check

    browser.close()

# собственно, сам тест
def test_points_on_map(fxtr_map):
    map_page, points_to_check = fxtr_map
    # проверяем что на карте отобразились только валидные точки из переданных
    assert map_page.check_points(points_to_check)

Запускаем.



Voila!

Благодаря mountebank’у мы прямо из фикстуры сконфигурировали mock-API так чтобы он отдавал определенное множество точек, дернули тестируемую страницу которая обратилась к мокируемому методу и получила заданные нами точки. Ну а в тестовой функции мы просто проверили что на экране пользователя отобразились только валидные точки. Благодаря возможности pytest параметризовывать фикстуру, нам даже не пришлось писать два теста: оба тестовых случая покрываются одной фикстурой и одним тестом.

В приведенном примере конфигурирование mock-API происходило напрямую через web API mountebank’a. В реальных проектах я рекомендую работать с mountebank на более высоком уровне абстракции, для чего написана масса клиентских библиотек. Для одного только python’а их аж три штуки, уверен, найдутся и для вашего любимого языка.

В данной статье я показал лишь малую часть возможностей mountebank’a. Помимо других базовых запросов для работы с imposter’ами, также остались нераскрытыми такие мощные штуки как проксирование запросов до API внешних систем, мокирование на уровне tcp протокола, js-injection.

Так что, если вас заинтересовал данный инструмент – добро пожаловать в документацию, которая, кстати, написана очень остроумным языком, подстать названию инструмента (mountebank – (с англ.) обманщик).

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