Вступительное слово
Немного объясню, зачем я вообще написал код для нагрузки на API и не воспользовался готовыми инструментами.
В своей работе я порой сталкиваюсь с задачами, которые, хоть и связаны с тестированием, но выходят за рамки моей специализации, например, тестирование производительности. Так, в один прекрасный день мне пришло задание нагрузить только что созданный GET-запрос, а именно — 50 rps в течение 20 секунд. Сначала я подумал сделать это в Postman, но в простой конфигурации можно указать только количество запросов и паузу между ними, а вкладка Performance встретила меня неприятным сообщением: «Couldn’t load form to set up performance test».
Следующим вариантом был JMeter. Хотя я делал с ним нагрузочные тесты, последний контакт с этим инструментом был аж 6 месяцев назад. Поэтому мне просто стало лень доставать его и вспоминать, как и что тут настраивать.
Вот тогда мне пришла идея написать собственный тестер нагрузки для API, который мог бы помочь QA-специалистам, не специализирующимся на нагрузочном тестировании, быстро решать аналогичные задачи, не углубляясь в сложные инструменты. По этой же причине я выбрал Node.js, так как он широко используется в современной разработке.
Реализация
Приступим к реализации. Для начала создадим папку проекта любым удобным для вас способом и инициализируем новый проект Node.js:
npm init -y
Затем устанавливаем необходимые зависимости:
npm install axios dotenv
Поскольку хорошие манеры предполагают выносить важные данные в переменные окружения, создаем в корне проекта файл .env
и добавляем следующие переменные:
BASE_URL=<ваш_URL_API> TOKEN=<ваш_токен_доступа> # Опционально, если требуется авторизация
Если вы планируете размещать проект в репозитории, не забудьте создать файл .gitignore
и добавить в него строку .env
, чтобы избежать случайного коммита данных из этого файла.
Создаем папку results
, в которой будет храниться результат теста, а также файл testGet.js
, в котором начнем писать наш код.
testGet.js для GET-запросов
Первым шагом подключаем необходимые модули:
-
dotenv
для загрузки переменных окружения из файла.env
-
axios
для отправки HTTP-запросов. -
fs
для работы с файловой системой. -
path
для работы с путями.
require('dotenv').config(); const axios = require('axios'); const fs = require('fs'); const path = require('path');
Задаем основные параметры для тестирования. Здесь параметры requestsPerSecond
и durationInSeconds
настраиваются исходя из ваших задач.
const url = process.env.BASE_URL; const token = process.env.TOKEN; // Параметры теста, которые вы настраиваете исходя из своих потребностей const requestsPerSecond = 50; // Количество запросов в секунду const durationInSeconds = 20; // Продолжительность теста в секундах const totalRequests = requestsPerSecond * durationInSeconds; let completedRequests = 0;
Создаем папку для хранения результатов и очищаем файл перед началом теста. Также нам потребуется установить флаг для записи результатов: с его помощью будет решаться, будут ли записываться только ошибки или все запросы.
const resultsDir = path.join(__dirname, 'results'); if (!fs.existsSync(resultsDir)) { fs.mkdirSync(resultsDir); } const resultsFilePath = path.join(resultsDir, 'results.txt'); fs.writeFileSync(resultsFilePath, ''); // Флаг для записи результатов: true - все, false - только ошибки const logAllResponses = false;
Создаем массив параметров, который будет использоваться для формирования запросов. Параметры могут быть изменены в зависимости от требований теста.
Если достаточно чтобы все тесты были с одинаковыми параметрами:
const queryParams = [{ param1: 'value1', param2: 'valueA' }];
Если нужно разнообразить запросы:
const queryParams = [ { param1: 'value1', param2: 'valueA' }, { param1: 'value2', param2: 'valueB' }, { param1: 'value3', param2: 'valueC' }, ];
Если запрос вообще без параметров, оставляем пустой массив.
Для записи результатов создаем функцию logResponse
. Она сохраняет в файл номер запроса, его статус, время выполнения и тело ответа:
const logResponse = (requestNumber, status, responseBody, timeTaken) => { const logEntry = `Запрос ${requestNumber}\nСтатус: ${status}\nВремя ответа: ${timeTaken}ms\nТело ответа: ${JSON.stringify(responseBody)}\n\n`; fs.appendFileSync(resultsFilePath, logEntry); };
Создаем асинхронную функцию sendRequest
, которая выполняет HTTP-запрос:
-
Используем библиотеку axios.
-
Передаем токен для авторизации в заголовке.
-
Сохраняем время выполнения запроса.
-
В случае ошибки логируем статус и текст ошибки.
const sendRequest = async (params, requestNumber) => { const startTime = Date.now(); try { const response = await axios.get(url, { headers: { 'Authorization': `Bearer ${token}` }, params: params }); const timeTaken = Date.now() - startTime; if (logAllResponses) { logResponse(requestNumber, response.status, response.data, timeTaken); } completedRequests++; } catch (error) { const timeTaken = Date.now() - startTime; let status = error.response ? error.response.status : 'Неизвестная ошибка'; let responseBody = error.response ? error.response.data : error.message; logResponse(requestNumber, status, responseBody, timeTaken); } };
Теперь реализуем основной цикл отправки запросов:
-
Используем
setInterval
для отправки определенного количества запросов в секунду. -
Проверяем завершение теста и останавливаем интервал, если отправлено необходимое количество запросов.
-
Сохраняем итог теста в файл и выводим его в консоль.
const startTest = () => { const interval = setInterval(() => { for (let i = 0; i < requestsPerSecond; i++) { if (completedRequests < totalRequests) { // Если есть параметры, используем их if (queryParams.length > 0) { const params = queryParams[i % queryParams.length]; sendRequest(params, completedRequests + 1); } else { sendRequest(null, completedRequests + 1); } } } if (completedRequests >= totalRequests) { clearInterval(interval); const summary = `Тест завершен. Отправлено ${completedRequests} запросов.\n`; fs.appendFileSync(resultsFilePath, summary); console.log(summary.trim()); } }, 1000); }; // Запуск теста startTest();
Всё, наш тестировщик нагрузки для GET-запросов готов. Запустить его можно командой:node testGet.js
testPost.js для POST-запросов
Хотя мое первоначальное желание ограничивалось скриптом для GET-запросов, но раз я собрался писать для Хабра, то решил не останавливаться на этом и дополнить его скриптом для POST-запросов.
Итак, создаем файл testPost.js
и вставляем в него первую часть кода из testGet.js
, заменив лишь имя текстового файла для записи результата на post_results.txt
.
require('dotenv').config(); const axios = require('axios'); const fs = require('fs'); const path = require('path'); const url = process.env.BASE_URL; const token = process.env.TOKEN; // Параметры теста, которые вы настраиваете исходя из своих потребностей const requestsPerSecond = 50; // Количество запросов в секунду const durationInSeconds = 20; // Продолжительность теста const totalRequests = requestsPerSecond * durationInSeconds; let completedRequests = 0; // Создание директории и файла для хранения результатов const resultsDir = path.join(__dirname, 'results'); if (!fs.existsSync(resultsDir)) { fs.mkdirSync(resultsDir); } const resultsFilePath = path.join(resultsDir, 'post_results.txt'); fs.writeFileSync(resultsFilePath, ''); // Флаг для записи результатов: true - все, false - только ошибки const logAllResponses = false;
Создадим функцию sendPostRequest
, которая будет отвечать за отправку одного POST-запроса:
const sendPostRequest = async (data, requestNumber) => { const startTime = Date.now(); try { const response = await axios.post(url, data, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); const timeTaken = Date.now() - startTime; if (logAllResponses) { logResponse(requestNumber, response.status, response.data, timeTaken); } completedRequests++; } catch (error) { const timeTaken = Date.now() - startTime; const status = error.response ? error.response.status : 'Неизвестная ошибка'; const responseBody = error.response ? error.response.data : error.message; logResponse(requestNumber, status, responseBody, timeTaken); } };
И, наконец, функция для запуска теста с динамической генерацией данных. Динамические данные (например, id
) можно генерировать по вашему усмотрению: это может быть простой Math.random
, библиотека faker
или что-то другое. Модернизацию кода я оставляю на ваше усмотрение.
const startPostTest = () => { const dataTemplate = { // если данные статичные, записывает как обычно: key1: 'value1', key2: 'value2', // если данные динамичные, генерирует случайные значения, записывает так key3: () => 'value3' // где value3 например может быть Math.floor(Math.random() * 9) + 1 }; const generateData = (template) => { if (Array.isArray(template)) { return template.map(item => generateData(item)); } else if (typeof template === 'object' && template !== null) { const result = {}; for (let key in template) { const value = template[key]; if (typeof value === 'function') { result[key] = value(); } else if (typeof value === 'object') { result[key] = generateData(value); } else { result[key] = value; } } return result; } else { return template; } }; const interval = setInterval(() => { for (let i = 0; i < requestsPerSecond; i++) { if (completedRequests < totalRequests) { const requestData = generateData(dataTemplate); sendPostRequest(requestData, completedRequests + 1); } } if (completedRequests >= totalRequests) { clearInterval(interval); const summary = `Тест завершен. Отправлено ${completedRequests} POST-запросов.\n`; fs.appendFileSync(resultsFilePath, summary); console.log(summary.trim()); } }, 1000); }; // Запуск теста startPostTest();
Запускаем этот скрипт можно командой:node testPost.js
Заключение
Этот функционал полностью подходит для решения небольших задач по нагрузочному тестированию API. Если кто-то обнаружит недостатки или баги, буду признателен за любую обратную связь.
Так же привожу ссылку на этот проект в Github
ссылка на оригинал статьи https://habr.com/ru/articles/870154/
Добавить комментарий