Skip to main content
Back to course
k6: нагрузочное тестирование как система
4 / 1724%

Тест-данные и параметризация

20 минут

Проблема: откуда брать данные для тестов

При нагрузочном тестировании нужны реалистичные данные:

  • Тестовые пользователи (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 загружается один раз при старте. Изменения в файле не подхватываются.

Решения:

  1. Перезапустить тест — самый простой способ
  2. Использовать externally-controlled executor и обновлять данные через API
  3. Загружать данные из 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-тест с правильными данными.