Вступительное слово
Немного объясню, зачем я вообще написал код для нагрузки на 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}msnТело ответа: ${JSON.stringify(responseBody)}nn`;
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
Автор: AlextooG