Тест-данные и параметризация
Проблема: откуда брать данные для тестов
При нагрузочном тестировании нужны реалистичные данные:
- Тестовые пользователи (username, password, email)
- Товары/сущности (SKU, ID, параметры)
- Уникальные значения для каждой итерации
- Изоляция данных между параллельными запусками
Типичная ошибка: Использовать одни и те же данные во всех VU → искусственно высокий cache hit rate, нереалистичная нагрузка на БД.
Способ 1: JSON/CSV fixtures
JSON fixtures с SharedArray
// test-with-fixtures.js
import http from "k6/http";
import { check, sleep } from "k6";
import { SharedArray } from "k6/data";
// ВАЖНО: SharedArray загружается ОДИН РАЗ в init-контексте
// Экономит память: все VU используют одну копию данных
const users = new SharedArray("users", function () {
return JSON.parse(open("./data/users.json"));
});
const products = new SharedArray("products", function () {
return JSON.parse(open("./data/products.json"));
});
export const options = {
scenarios: {
load_test: {
executor: "ramping-vus",
stages: [
{ duration: "2m", target: 50 },
{ duration: "5m", target: 50 },
{ duration: "2m", target: 0 },
],
},
},
};
const BASE_URL = __ENV.BASE_URL || "https://test-api.k6.io";
export default function () {
// Каждый VU получает данные по кругу
const user = users[__VU % users.length];
const product = products[__ITER % products.length];
// Login
const loginRes = http.post(
`${BASE_URL}/auth/login`,
JSON.stringify({
username: user.username,
password: user.password,
}),
{ headers: { "Content-Type": "application/json" } }
);
check(loginRes, {
"login successful": (r) => r.status === 200,
});
const token = loginRes.json("access_token");
// Use product data
const res = http.get(`${BASE_URL}/products/${product.id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
check(res, {
"product loaded": (r) => r.status === 200,
});
sleep(1);
}data/users.json:
[
{ "username": "user1", "password": "pass123", "email": "user1@example.com" },
{ "username": "user2", "password": "pass123", "email": "user2@example.com" },
{ "username": "user3", "password": "pass123", "email": "user3@example.com" }
]data/products.json:
[
{ "id": "PROD-001", "sku": "SHOES-001", "name": "Running Shoes" },
{ "id": "PROD-002", "sku": "SHIRT-002", "name": "T-Shirt" },
{ "id": "PROD-003", "sku": "WATCH-003", "name": "Sport Watch" }
]SharedArray vs обычный массив:
- Обычный массив: копируется в каждый VU → 100 VU × 10MB = 1GB RAM
- SharedArray: загружается один раз → 10MB RAM для всех VU
Используйте SharedArray для больших наборов данных!
CSV fixtures
import { SharedArray } from "k6/data";
import papaparse from "https://jslib.k6.io/papaparse/5.1.1/index.js";
const users = new SharedArray("users", function () {
return papaparse.parse(open("./data/users.csv"), { header: true }).data;
});
export default function () {
const user = users[__VU % users.length];
console.log(`User: ${user.username}, Email: ${user.email}`);
}data/users.csv:
username,password,email
user1,pass123,user1@example.com
user2,pass123,user2@example.com
user3,pass123,user3@example.comСпособ 2: Динамическая генерация данных
Генерация уникальных ID
import {
randomString,
randomIntBetween,
} from "https://jslib.k6.io/k6-utils/1.2.0/index.js";
export default function () {
// Уникальный ID на основе VU + iteration
const uniqueId = `${Date.now()}-${__VU}-${__ITER}`;
// Случайные данные
const email = `user-${randomString(8)}@example.com`;
const age = randomIntBetween(18, 65);
const amount = randomIntBetween(10, 500);
const payload = JSON.stringify({
id: uniqueId,
email: email,
age: age,
amount: amount,
});
const res = http.post(`${BASE_URL}/api/orders`, payload, {
headers: { "Content-Type": "application/json" },
});
check(res, {
"order created": (r) => r.status === 201,
});
}Генератор тестовых данных в setup()
import { randomString } from "https://jslib.k6.io/k6-utils/1.2.0/index.js";
export function setup() {
// Генерируем данные один раз перед тестом
const testUsers = [];
for (let i = 0; i < 100; i++) {
testUsers.push({
username: `user-${i}`,
email: `user-${randomString(8)}@example.com`,
password: "password123",
});
}
return { testUsers };
}
export default function (data) {
const user = data.testUsers[__VU % data.testUsers.length];
// Используем сгенерированные данные
const res = http.post(`${BASE_URL}/auth/register`, JSON.stringify(user), {
headers: { "Content-Type": "application/json" },
});
check(res, {
"user registered": (r) => r.status === 201,
});
}Способ 3: Пулы тестовых пользователей
Предварительно созданные пользователи
import { SharedArray } from "k6/data";
const users = new SharedArray("users", function () {
// В реальности загружаем из БД или API
return JSON.parse(open("./data/test-users.json"));
});
export default function () {
// Round-robin: каждый VU получает своего пользователя
const user = users[__VU % users.length];
// Логинимся под тестовым пользователем
const res = http.post(
`${BASE_URL}/auth/login`,
JSON.stringify({
username: user.username,
password: user.password,
}),
{ headers: { "Content-Type": "application/json" } }
);
const token = res.json("access_token");
// Дальнейшие действия от имени этого пользователя
http.get(`${BASE_URL}/api/profile`, {
headers: { Authorization: `Bearer ${token}` },
});
}Изоляция данных между запусками
Префикс RUN_ID
const RUN_ID = __ENV.RUN_ID || Date.now();
export default function () {
// Все сущности помечены RUN_ID
const orderId = `order-${RUN_ID}-${__VU}-${__ITER}`;
const cartId = `cart-${RUN_ID}-${__VU}`;
const res = http.post(
`${BASE_URL}/api/orders`,
JSON.stringify({
orderId: orderId,
cartId: cartId,
items: [{ sku: "PROD-001", qty: 1 }],
}),
{ headers: { "Content-Type": "application/json" } }
);
check(res, {
"order created": (r) => r.status === 201,
});
}Cleanup после теста
export function teardown(data) {
const RUN_ID = __ENV.RUN_ID || data.runId;
// Удаляем все сущности, созданные в этом прогоне
http.del(`${BASE_URL}/api/cleanup?run_id=${RUN_ID}`, {
headers: { "X-Admin-Key": __ENV.ADMIN_KEY },
});
console.log(`Cleanup completed for RUN_ID: ${RUN_ID}`);
}Продвинутые паттерны
Rotation pool (ротация пользователей)
import { SharedArray } from "k6/data";
const users = new SharedArray("users", function () {
return JSON.parse(open("./data/users.json"));
});
let userIndex = 0;
export default function () {
// Sequential access: каждая итерация берет следующего пользователя
const user = users[userIndex % users.length];
userIndex++;
// Логинимся
const res = http.post(`${BASE_URL}/auth/login`, JSON.stringify(user), {
headers: { "Content-Type": "application/json" },
});
check(res, {
"login successful": (r) => r.status === 200,
});
}Weighted random (взвешенный случайный выбор)
import { SharedArray } from "k6/data";
const products = new SharedArray("products", function () {
return [
{ id: "PROD-001", weight: 50 }, // 50% популярности
{ id: "PROD-002", weight: 30 }, // 30%
{ id: "PROD-003", weight: 15 }, // 15%
{ id: "PROD-004", weight: 5 }, // 5%
];
});
function weightedRandom(items) {
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
let random = Math.random() * totalWeight;
for (const item of items) {
random -= item.weight;
if (random <= 0) {
return item;
}
}
return items[items.length - 1];
}
export default function () {
const product = weightedRandom(products);
const res = http.get(`${BASE_URL}/products/${product.id}`);
check(res, {
"product loaded": (r) => r.status === 200,
});
}Troubleshooting: типичные проблемы
Причина: Пытаетесь загрузить файл внутри default function (VU context). open() работает только в init-контексте.
Решение: Используйте SharedArray в init-контексте:
// ❌ Неправильно
export default function() {
const data = JSON.parse(open("data.json")); // Ошибка!
}
// ✅ Правильно
const data = new SharedArray("data", function() {
return JSON.parse(open("data.json"));
});
export default function() {
const item = data[__ITER % data.length];
}Причина: Используете обычный массив вместо SharedArray.
Решение:
- Замените обычные массивы на SharedArray
- Не копируйте данные в каждом VU
- Генерируйте данные динамически, если объем очень большой
// ❌ Плохо: 100 VU × 10MB = 1GB
const users = JSON.parse(open("users.json"));
// ✅ Хорошо: 10MB для всех VU
const users = new SharedArray("users", () =>
JSON.parse(open("users.json"))
);Причина: Используете одинаковые ID/username без изоляции.
Решение: Добавьте префикс RUN_ID:
const RUN_ID = __ENV.RUN_ID || Date.now();
export default function() {
const uniqueId = `order-${RUN_ID}-${__VU}-${__ITER}`;
http.post(`${BASE_URL}/api/orders`,
JSON.stringify({ orderId: uniqueId })
);
}
// Запуск с RUN_ID
// RUN_ID=test-123 k6 run test.jsПроблема: SharedArray загружается один раз при старте. Изменения в файле не подхватываются.
Решения:
- Перезапустить тест — самый простой способ
- Использовать externally-controlled executor и обновлять данные через API
- Загружать данные из API в setup() вместо файлов
export function setup() {
// Загружаем свежие данные из API перед каждым запуском
const res = http.get(`${BASE_URL}/api/test-users`);
return { users: res.json() };
}
export default function(data) {
const user = data.users[__VU % data.users.length];
}✅ Чек-лист завершения урока
После этого урока вы должны уметь:
Работа с fixtures:
- Загружать JSON/CSV файлы через SharedArray
- Понимать разницу между обычным массивом и SharedArray
- Использовать round-robin для распределения данных между VU
Динамическая генерация:
- Генерировать уникальные ID:
Date.now() + '-' + __VU + '-' + __ITER - Использовать randomString, randomIntBetween для случайных данных
- Создавать данные в setup() для переиспользования
Изоляция и cleanup:
- Префиксовать данные RUN_ID для изоляции запусков
- Реализовать cleanup в teardown()
- Управлять пулами тестовых пользователей
Продвинутые паттерны:
- Weighted random для реалистичного распределения
- Sequential vs random доступ к данным
- Оптимизация памяти для больших datasets
Практическое задание:
- Создайте users.json и products.json для вашего проекта
- Загрузите их через SharedArray
- Реализуйте уникальные ID с RUN_ID
- Добавьте cleanup в teardown()
Если чек-лист пройден — переходите к уроку 05: напишем первый smoke-тест с правильными данными.