В отличие от ассёртов Playwright, упавшая проверка k6 не останавливает тест: все виртуальные пользователи продолжают работать, а k6 считает процент прошедших и упавших проверок по тысячам запросов и показывает итоговый сигнал здоровья в сводке. Останавливает запуск нарушение порога: если p95 времени ответа превышает заданный лимит, k6 завершается с кодом 99 и шаг CI падает точно так же как упавший тест. Гайд охватывает установку k6, написание первого скрипта с реалистичными паузами sleep(), чтение сводной таблицы вывода, настройку стадий наращивания нагрузки и пороги которые делают регрессии производительности видимыми в CI.
Что такое k6 и зачем QA-инженеру его знать
k6: инструмент нагрузочного тестирования с открытым исходным кодом от Grafana Labs. Скрипты пишутся на JavaScript или TypeScript, запускаются из CLI, на выходе: подробная информация о поведении системы под нагрузкой. Репозиторий на GitHub набрал больше 24 000 звёзд и используется в компаниях любого масштаба.
Для QA-инженеров k6 удобен моделью написания скриптов. Если уже пишешь тесты на Playwright или Jest, синтаксис знакомый: импорт модулей, функции, HTTP-запросы, ассёрты. Никакого тяжёлого GUI, никакого проприетарного формата записи, никаких XML-конфигов. Только скрипт.
k6 чисто встраивается в GitHub Actions. Устанавливаешь бинарник, запускаешь скрипт, и если тест падает (подробнее о том что значит «падает» ниже), шаг CI завершается с ненулевым кодом. Тот же контракт что у любого другого тест-инструмента.
k6 не замена Playwright. Playwright проверяет поведение, k6 измеряет производительность под нагрузкой. Разные вопросы, оба нужны в зрелой QA-стратегии.
Установка k6 и первый тест
k6 поставляется как самодостаточный бинарник. На macOS:
brew install k6На Linux (Debian/Ubuntu):
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6На Windows: официальный установщик с k6.io/docs/get-started/installation или через Chocolatey:
choco install k6Проверяем установку:
k6 versionСоздаём файл load-test.js с минимальным скриптом:
import http from 'k6/http';
export default function () {
http.get('https://test-api.k6.io/public/crocodiles/');
}Запускаем:
k6 run load-test.jsk6 использует test-api.k6.io как публичную песочницу. Можно запускать без предварительной настройки. В терминале появится вывод: количество запросов, время ответа и сводная таблица. Первый нагрузочный тест запущен.
Анатомия k6-скрипта
Полноценный k6-скрипт состоит из трёх частей: экспорт options который управляет поведением теста, функция default с тестовой логикой, и любая подготовка нужная запросам. Структура:
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 10, // виртуальных пользователей параллельно
duration: '30s', // сколько длится тест
};
export default function () {
http.get('https://your-api.example.com/api/products');
sleep(1); // пауза между запросами
}vus означает виртуальных пользователей. Каждый VU запускает функцию default в цикле на всё время теста, независимо от остальных. vus: 10 и duration: '30s' означают 10 симулированных пользователей которые непрерывно нагружают эндпоинт 30 секунд.
sleep(1) не опционален: он моделирует время которое реальный пользователь тратит между действиями. Без него каждый VU шлёт запросы так быстро как сервер успевает отвечать: нереалистично и даёт завышенные цифры пропускной способности. Секундная пауза: разумный дефолт, корректируй под реальное поведение пользователей.
Для POST-запросов с JSON-телом (нужны для большинства API-тестирования):
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 20,
duration: '1m',
};
export default function () {
const url = 'https://your-api.example.com/api/login';
const payload = JSON.stringify({
username: 'testuser@example.com',
password: 'testpassword',
});
const params = {
headers: { 'Content-Type': 'application/json' },
};
http.post(url, payload, params);
sleep(1);
}Чтение вывода k6
После прогона k6 печатает сводную таблицу в терминал. Умение быстро её читать превращает сырые числа в понятную информацию.
Типичный вывод:
✓ checks.........................: 100.00% ✓ 1800 ✗ 0
data_received..................: 4.3 MB 143 kB/s
data_sent......................: 523 kB 17 kB/s
http_req_blocked...............: avg=1.2ms min=1µs med=4µs max=312ms p(90)=7µs p(95)=11µs
http_req_connecting............: avg=743µs min=0s med=0s max=181ms p(90)=0s p(95)=0s
http_req_duration..............: avg=312ms min=98ms med=280ms max=2.1s p(90)=520ms p(95)=640ms
{ expected_response:true }...: avg=312ms min=98ms med=280ms max=2.1s p(90)=520ms p(95)=640ms
http_req_failed................: 0.00% ✓ 0 ✗ 1800
http_req_receiving.............: avg=2.1ms min=45µs med=1.1ms max=74ms p(90)=4.2ms p(95)=5.8ms
http_req_sending...............: avg=178µs min=27µs med=136µs max=3.2ms p(90)=312µs p(95)=389µs
http_req_tls_handshaking.......: avg=412µs min=0s med=0s max=168ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=310ms min=97ms med=278ms max=2.1s p(90)=518ms p(95)=637ms
http_reqs......................: 1800 60.0/s
iteration_duration.............: avg=1.31s min=1.1s med=1.28s max=3.1s p(90)=1.52s p(95)=1.64s
iterations.....................: 1800 60.0/s
vus............................: 10 min=10 max=10
vus_max........................: 10 min=10 max=10Три метрики которые смотришь первыми:
http_req_duration: полное время round-trip каждого запроса. Колонка p(95) показывает 95-й перцентиль: 95% запросов завершились за это время. Это число важно для SLA, не среднее. Среднее скрывает выбросы, p95 нет.
http_reqs: общее количество запросов и частота в секунду. Показывает сколько нагрузки реально создано.
http_req_failed: доля запросов получивших ответ с ошибкой (4xx или 5xx). Ноль процентов норма, всё что выше требует расследования.
Проверки (checks)
Функциональная корректность под нагрузкой не менее важна чем скорость. В k6 есть встроенный механизм проверок похожий на expect в других тест-фреймворках:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '30s',
};
export default function () {
const res = http.get('https://your-api.example.com/api/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'body contains products': (r) => r.body.includes('"id"'),
});
sleep(1);
}Главное отличие от обычных ассёртов: упавшая проверка не останавливает тест. Все VU продолжают работать. k6 считает сколько проверок прошло и упало по всем итерациям и показывает процент в сводке. Это намеренно: нужно знать какой процент запросов под нагрузкой дал корректный ответ, а не просто упал ли один запрос.
Для эндпоинта логина возвращающего токен:
import http from 'k6/http';
import { check, sleep } from 'k6';
export default function () {
const payload = JSON.stringify({
username: 'user@example.com',
password: 'password123',
});
const res = http.post('https://your-api.example.com/auth/login', payload, {
headers: { 'Content-Type': 'application/json' },
});
check(res, {
'login succeeds': (r) => r.status === 200,
'token present': (r) => JSON.parse(r.body).token !== undefined,
'login fast enough': (r) => r.timings.duration < 1000,
});
sleep(1);
}Стадии: наращивание, плато, снижение
Постоянная плоская нагрузка редко нужна. Реальный трафик наращивается по мере прихода пользователей, держится на пике и падает. k6 моделирует это через стадии:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 50 }, // нагружаем до 50 VU за 2 минуты
{ duration: '5m', target: 50 }, // держим 50 VU 5 минут
{ duration: '2m', target: 100 }, // нагружаем до 100 VU за 2 минуты
{ duration: '5m', target: 100 }, // держим 100 VU 5 минут
{ duration: '2m', target: 0 }, // снижаем до 0
],
};
export default function () {
const res = http.get('https://your-api.example.com/api/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time OK': (r) => r.timings.duration < 1000,
});
sleep(1);
}Этот паттерн (наращивание, плато, ещё наращивание, плато, снижение) называется стресс-профилем. Он показывает при каком количестве пользователей система начинает деградировать. В результатах p95 будет планомерно расти по мере увеличения VU. Точка резкого скачка: предел системы.
Для большинства API-тестирования достаточно простого двухфазного теста: наращивание и плато.
export const options = {
stages: [
{ duration: '1m', target: 50 }, // прогрев
{ duration: '5m', target: 50 }, // устойчивая нагрузка
{ duration: '30s', target: 0 }, // снижение
],
};Фаза снижения важна. Резкое отключение нагрузки может скрыть проблемы с пулом соединений и даёт чище метрики в конце теста.
Пороги: прохождение и провал теста
Проверки говорят что произошло. Пороги определяют прошёл ли тест. Порог: условие прохождения/провала для любой метрики. Если условие нарушено, k6 завершается с ненулевым кодом. Это и делает нагрузочные тесты полезными в CI.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '30s', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<2000'], // 95-й перцентиль должен быть меньше 2 секунд
http_req_failed: ['rate<0.01'], // менее 1% запросов могут завершиться с ошибкой
checks: ['rate>0.99'], // больше 99% проверок должны пройти
},
};
export default function () {
const res = http.get('https://your-api.example.com/api/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 2s': (r) => r.timings.duration < 2000,
});
sleep(1);
}Когда p95 превышает 2000 мс, запуск завершается с кодом 99. Шаг CI падает. Это правильное поведение. Сборка которая делает API на 40% медленнее должна падать.
Пороги можно задавать для конкретных эндпоинтов через теги:
export default function () {
const loginRes = http.post(
'https://your-api.example.com/auth/login',
JSON.stringify({ username: 'user@example.com', password: 'pass' }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'login' } }
);
const productsRes = http.get(
'https://your-api.example.com/api/products',
{ tags: { name: 'products' } }
);
}export const options = {
thresholds: {
'http_req_duration{name:login}': ['p(95)<1000'],
'http_req_duration{name:products}': ['p(95)<500'],
},
};Логин ожидаемо медленнее чем закешированный список продуктов, поэтому у каждого эндпоинта свой SLA.
k6 в GitHub Actions
k6 чисто работает в GitHub Actions. Команда Grafana публикует официальный экшен который устанавливает бинарник и фиксирует версию:
# .github/workflows/load-test.yml
name: Load Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
load-test:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install k6
uses: grafana/setup-k6-action@v1
with:
k6-version: '0.55.0'
- name: Run load test
run: k6 run load-test.js
env:
BASE_URL: ${{ vars.BASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
- name: Upload k6 results
uses: actions/upload-artifact@v4
if: always()
with:
name: k6-results
path: k6-results.json
retention-days: 14Для доступа к переменным окружения внутри скрипта используй __ENV:
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export const options = {
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '30s', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<2000'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get(`${BASE_URL}/api/products`);
check(res, {
'status is 200': (r) => r.status === 200,
'response time acceptable': (r) => r.timings.duration < 2000,
});
sleep(1);
}Для генерации JSON-файла результатов используй флаг --out:
k6 run --out json=k6-results.json load-test.jsПрактическая рекомендация: не запускай нагрузочные тесты на каждый пул-реквест против продакшна или общего стейджинга. Нагрузочный тест должен запускаться на мерджах в main или вручную через workflow_dispatch. Тяжёлые нагрузочные тесты на каждой ветке PR создают интерференцию между параллельными запусками. Smoke-тест (5 VU, 30 секунд) на PR приемлем, полный нагрузочный тест нет.
Какие эндпоинты тестировать
Выбор того что тестировать с k6 не менее важен чем сам тест. Не каждому эндпоинту нужен нагрузочный тест. Фокус на тех где производительность важна.
Эндпоинт логина пишешь первым. С него начинается каждая пользовательская сессия. Медленный логин умноженный на 500 параллельных пользователей разрушает опыт ещё до того как кто-то дошёл до функционала. Тестируй полный флоу логина: POST-запрос с учётными данными, получение токена, один аутентифицированный запрос.import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<1500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
// Шаг 1: Логин
const loginRes = http.post(
`${BASE_URL}/auth/login`,
JSON.stringify({ username: 'loadtest@example.com', password: 'loadtestpass' }),
{ headers: { 'Content-Type': 'application/json' } }
);
check(loginRes, {
'login status 200': (r) => r.status === 200,
'token returned': (r) => JSON.parse(r.body).token !== undefined,
});
const token = JSON.parse(loginRes.body).token;
// Шаг 2: Аутентифицированный запрос
const profileRes = http.get(`${BASE_URL}/api/me`, {
headers: { Authorization: `Bearer ${token}` },
});
check(profileRes, {
'profile status 200': (r) => r.status === 200,
});
sleep(2);
}Разумная точка старта для большинства приложений: три нагрузочных скрипта. Один для аутентификации, один для трёх главных read-эндпоинтов в одном скрипте, один для критически важной конверсионной write-операции. Этого покрытия достаточно чтобы ловить значимые регрессии производительности без построения полноценного нагрузочного сьюта до получения базовых метрик.
FAQ
Нужна ли отдельная среда для запуска k6?
Нужна среда которая выдержит нагрузку без влияния на реальных пользователей и порчи продакшн-данных. Отдельная среда для нагрузочных тестов идеальна. Если только стейджинг: запускай тесты в нерабочее время и убедись что тестовые данные изолированы. На продакшн с большим количеством VU никогда не направляй.
Сколько VU использовать?
Начни с числа которое представляет реалистичный пиковый трафик, а не теоретический максимум. Если в приложении 50 параллельных пользователей в напряжённый день, тестируй на 50–100. Знать что происходит при 1000 VU менее важно чем убедиться что система комфортно справляется с нормальной пиковой нагрузкой.
k6-тест проходит, но приложение ощущается медленным. Почему?
k6 измеряет время HTTP-ответа: серверная обработка включена, клиентский рендеринг нет. API-эндпоинт который отвечает за 200 мс может всё равно ощущаться медленным если фронтенд делает 20 последовательных вызовов для заполнения страницы. k6 для производительности на уровне API, браузерные профилировщики для воспринимаемой производительности страницы.
Поддерживает ли k6 WebSocket или gRPC?
Да. k6 поддерживает WebSocket через модуль k6/ws и gRPC через k6/net/grpc. Модель написания скриптов одинаковая, отличается только протокол-специфичный API.
Как поделиться результатами k6 с командой?
JSON-файл из --out json можно импортировать в Grafana для визуализации. Если команда использует Grafana Cloud, k6 имеет нативную интеграцию которая стримит результаты в реальном времени и хранит их для сравнения между запусками. Для команд без Grafana сводная таблица из терминала в комментарий PR или в Slack достаточна для передачи статуса прохождения и ключевых перцентилей.
В чём разница между нагрузочным и стресс-тестом?
Нагрузочный тест проверяет что система выполняет требования к производительности при ожидаемом трафике. Стресс-тест выходит за ожидаемые пределы чтобы найти точку отказа. Конфигурация стадий обрабатывает оба: нагрузочный тест держится на целевом количестве VU, стресс-тест продолжает наращивать пока проверки не начнут падать.
→ See also: Основы нагрузочного тестирования: нагрузочное, стресс и спайк-тестирование | API-тестирование 101: всё, что нужно знать QA-инженеру в 2026 году | CI/CD для QA: сравнение GitHub Actions, Jenkins и GitLab