Разделы портала

Онлайн-тренинги

.
API-имитация на JavaScript с Pactum
25.08.2022 00:00

Автор: Баз Дейкстра (Bas Dijkstra)
Оригинал статьи
Перевод: Ольга Алифанова

Недавно я написал статью про Pactum, библиотеку JavaScript для тестирования API, имитации и контрактного тестирования. В статье я сконцентрировался на возможностях Pactum для тестирования API. Сегодня я хочу продолжить изучать Pactum, внимательнее рассмотрев его функциональность по имитации API.

У меня есть опыт работы с библиотеками имитации API, особенно с WireMock и WireMock.Net, и в сегодняшней статье я сравню с ними Pactum, рассмотрев важные функции, которые необходимы мне в любом инструменте или библиотеке, имитирующих API.


Подготовка: настройка и тестирование первого имитатора

Начнем с простого примера "Привет, мир". Как и WireMock и WireMock.Net, Pactum предоставляет сервер-имитатор, на который можно добавлять имитации ответов. Как и я, пользователи вышеупомянутых инструментов не должны испытывать сложностей, начиная работу с Pactum.

Стартовать сервер-имитатор Pactum легко:

const {mock, settings } = require('pactum');
 beforeEach(async () => {
  settings.setLogLevel('ERROR');
  await mock.start(9876);
});

Я использую конструкцию Jest beforeEach, чтобы стартовать сервер-имитатор перед каждым тестом. Затем я устанавливаю уровень логирования на ERROR, дабы избавиться от логов старта и выключения, которые Pactum выводит в консоль по умолчанию – они мне не нужны. И, наконец, я стартую сервер, порт 9876. Вот и все.

Выключение сервера после каждого теста настолько же очевидно:

afterEach(async () => {
  await mock.stop()
});

Если вы хотите запустить/выключить сервер лишь один раз, перед прогоном теста, вы можете заменить beforeEach и afterEach на beforeAll и after All, соответственно. Предоставлю это вам. Старт и остановка сервера – это очень, очень быстро, поэтому я не заметил никакого снижения производительности, включая его перед каждым тестом.

Теперь, когда мы можем стартовать и выключить сервер, добавим туда первую имитацию ответа:

function addHelloWorldResponse() {
    mock.addInteraction({
        request: {
            method: 'GET',
            path: '/api/hello-world'
        },
        response: {
            status: 200,
            body: 'Hello, world!'
        }
    });
}

Имитации ответов добавляются на сервер-имитатор Pactum через интеракции. Интеракция содержит информацию о запросе, на который надо ответить (используемой для проверки совпадения запросов, я к этому еще вернусь), а также имитацию ответа, который надо вернуть. В этом случае мы хотим ответить на запрос HTTP GET к /api/hello-world ответом с кодом статуса HTTP 200 и чистым текстом в теле "Hello, world!"

Чтобы проверить, работает ли это, напишем тест в Pactum, вызывающий имитированную конечную точку на нашей локальной машине, порт 9876:

const pactum = require('pactum');
describe('Demonstrating that Pactum API mocking can', () => {
    test('return a basic REST response', async () => {
        addHelloWorldResponse();
        await pactum.spec()
            .get('http://localhost:9876/api/hello-world')
            .expectStatus(200)
            .expectBody('Hello, world!')
    });
});

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


Сверка запросов

В предыдущем примере сверка запросов (то есть изучение характеристик входящего запроса, чтобы определить подходящий ответ) выполнялась через изучение HTTP-глагола (GET) и конечной точки (/api/hello-world). Pactum предлагает и другие стратегии сверки запросов, включая сверку по заголовкам запросов и их значениям (удобно для авторизации), параметрам запросов и их значениям, и содержанию тела запроса.

Вот пример, как добавить ответы на запросы с определенным параметром:

function addQueryParameterRequestMatchingResponses() {
    mock.addInteraction({
        request: {
            method: 'GET',
            path: '/api/zip',
            queryParams: {
                zipcode: 90210
            }
        },
        response: {
            status: 200,
            body: {
                zipcode: 90210,
                city: 'Beverly Hills'
            }
        }
    });
mock.addInteraction({
        request: {
            method: 'GET',
            path: '/api/zip',
            queryParams: {
                zipcode: 12345
            }
        },
        response: {
            status: 200,
            body: {
                zipcode: 12345,
                city: 'Schenectady'
            }
        }
    });
}

Тем самым мы командуем серверу Pactum ответить на:

  • HTTP GET к /api/zip?zipcode=90210 с телом ответа {zipcode: 90210, city: 'Beverly Hills'}
  •  HTTP GET к /api/zip?zipcode=12345 с телом ответа {zipcode:12345, city: 'Schenectady}
  • Все прочие запросы (включая запросы к /api/zip с различными значениями параметра) – ошибкой HTTP 404 (ответ по умолчанию для запроса без соответствий).

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

Симуляция производительности

Другая полезная функция любой библиотеки имитации API – это способность задавать поведение производительности, или возможность определять, сколько сервер должен ждать перед ответом на входящий запрос. Это определение имитации, к примеру, возвращает ответ после фиксированного ожидания в 1000 миллисекунд:

function addDelayedResponse() {
    mock.addInteraction({
        request: {
            method: 'GET',
            path: '/api/delay'
        },
        response: {
            status: 200,
            fixedDelay: 1000
        }
    })
}

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

Этот тест, на который имитатор отвечает с задержкой, падает:

test('return a REST response with a delay', async () => {
    addDelayedResponse();
await pactum.spec()
     .get('http://localhost:9876/api/delay')
        .expectStatus(200)
        .expectResponseTime(1000)
});

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

Повторное использование значений запроса

Зачастую при имитации ответа API нужно повторно использовать значения из запроса (уникальные ID, куки, другие динамические значения). Pactum позволяет и это:

const { like } = require('pactum-matchers');
function addReusePathParameterValueResponse() {
mock.addInteraction({
        request: {
            method: 'GET',
            path: '/api/user/{id}',
            pathParams: {
                id: like('random-id')
            }
        },
        stores: {
            userId: 'req.pathParams.id'
        },
        response: {
            status: 200,
            body: {
                message: `Returning data for user $S{userId}`
            }
        }
    });
}

Это определение имитации задает специфический сегмент пути как параметр пути id (что относится к ID пользователя), и сохраняет его для повторного использования с именем userId. Затем его можно использовать вновь, создавая ответ, что мы делаем, используя его в шаблоне строки и задействуя ранее сохраненную переменную через $S{userId}. Пожалуйста, обратите внимание на букву S – предполагаю, она относится к чему-то вроде хранилища (Store), где в Pactum хранятся значения.

Этот управляемый через данные тест (пожалуйста, посмотрите предыдущую статью) показывает, что сервер-имитатор Pactum успешно изымает значение параметра пути из запроса и использует его в теле ответа:

test.each(
[[1], [2], [3]]
)('use response templating to return the expected message for user %i', async (userId) => {
addReusePathParameterValueResponse();
await pactum.spec()
       .get('http://localhost:9876/api/user/{user}')
        .withPathParams('user', userId)
        .expectStatus(200)
        .expectJsonMatch('message', `Returning data for user ${userId}`)
});

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

Закончу на том, что, с моей точки зрения, возможности Pactum по имитации API легко использовать – хотя, возможно, мне помогает предыдущий опыт работы с WireMock.

В статье я не затронул возможность имитации состояний – то есть моделирования "состояний" или "памяти" API. WireMock делает это, используя модели с конечным числом состояний – возможно, в Pactum можно сделать нечто похожее при помощи конструкции onCall, как показано тут.

Этот подход отличается от подходов WireMock и WireMock.Net, но при простых сценариях вы должны получить похожие результаты.

Весь код из статьи можно найти на GitHub.

Обсудить в форуме

afterEach(async () => {

 

    await mock.stop()

});