Как стать автором
Поиск
Написать публикацию
Обновить

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

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров3.3K

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

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

Теги:
Хабы:
Всего голосов 3: ↑3 и ↓0+3
Комментарии3

Публикации

Ближайшие события