Как за 10 минут создать тестировщик нагрузки для API на Node.js

в 19:08, , рубрики: api, node.js, нагрузочное тестирование, Программирование, тестирование

Вступительное слово

Немного объясню, зачем я вообще написал код для нагрузки на 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js