Supplying test data from CSV and JSON with SharedArray and papaparse, randomizing input via randomItem, correlating data between requests, and keeping data clean so load tests are always repeatable.

In episode 7 you succeeded in separating configuration from code: one script, many environments. But note one remaining gap — credentials, URLs, and payloads are still written by hand inside the script. Five VUs can safely share one account; five hundred VUs using the same account will confuse the server, make logins collide, and dirty the data. Real users don't share accounts — why does your simulation?
This episode teaches data-driven testing: separating test data from the script, supplying it from CSV and JSON files, and using data realistically. We'll use SharedArray from the k6/data module, the papaparse parser from jslib, and randomItem from k6-utils. More than just syntax, this episode covers a philosophy that's often overlooked: data is part of the test results. Exhausted, dirty, or non-unique data produces wrong conclusions — no matter how great the script is.
In episode 6 you learned that k6 modules aren't Node.js. The consequence: a plain array loaded in the init phase is copied into every VU's memory. With 1,000 VUs and a 10 MB data file, k6 needs 10 GB. The solution is SharedArray from the k6/data module — one copy of the data shared across all VUs:
import { SharedArray } from "k6/data";
const users = new SharedArray("users", function () {
return JSON.parse(open("./users.json"));
});
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
console.log(`VU ${__VU} memakai akun ${user.email}`);
}Note the construction: new SharedArray("name", function() { ... }). The second function executes in the init context to load the data — open("./users.json") reads the file and JSON.parse turns it into an array of objects. The first name ("users") is just a label for internal identification.
The SharedArray ground rules you must remember:
Credential data often comes from spreadsheets, and its natural format is CSV. k6 has no built-in CSV parser, so we use papaparse from jslib — a standard library hosted by the k6 team that can be imported directly as a URL:
email,password,nama
arif@example.com,rahasia1,Arif
dewi@example.com,rahasia2,Dewi
bima@example.com,rahasia3,Bimaimport papaparse from "https://jslib.k6.io/papaparse/5.1/k6-parser.js";
import { SharedArray } from "k6/data";
const users = new SharedArray("users", function () {
return papaparse.parse(open("./users.csv"), { header: true }).data;
});
export default function () {
const user = users[Math.floor(Math.random() * users.length)];
console.log(`${user.nama} login sebagai ${user.email}`);
}What determines papaparse's behavior is the { header: true } option. Without this option, the result is an array of arrays — users[0][1] for the email, hard to read and fragile to column order changes. With header: true, every row becomes an object with column names as keys — user.email, user.nama. Always turn this option on unless you genuinely need raw data.
The combination of open (reading a file in init), papaparse (parsing CSV), and SharedArray (sharing across all VUs) is the standard pipeline for data-driven testing in k6. These three parts each have one role — learn this pattern, because it will keep appearing throughout your career.
Picking random data with Math.random() works, but there's a more expressive way: randomItem from k6-utils, one of the official jslib libraries:
import { randomItem } from "https://jslib.k6.io/k6-utils/1.6.0/index.js";
const kota = ["Jakarta", "Bandung", "Surabaya", "Medan", "Yogyakarta"];
export default function () {
const asal = randomItem(kota);
const tujuan = randomItem(kota.filter((k) => k !== asal));
console.log(`Perjalanan ${asal} ke ${tujuan}`);
}Two things k6-utils offers: readability (the code's intent is clearer than the Math.floor(Math.random() * length) formula) and complementary utilities like randomIntBetween, randomString, and uuidv4 that you'll meet in many production scripts. The example above also shows a simple correlation technique: randomItem is used twice with a filter so origin and destination are never the same — the chosen data is interdependent, like in the real world.
This is the bridge to episode 5. Data-driven testing doesn't stop at choosing input — it's also about correlation: a value created by one request becomes the input of the next one. The classic example: create a resource, then use its id:
import http from "k6/http";
import { check } from "k6";
import { randomItem } from "https://jslib.k6.io/k6-utils/1.6.0/index.js";
const titles = ["Belajar k6", "Dasar HTTP", "Skenario Lanjutan", "Data Driven"];
export default function () {
const createRes = http.post("https://api.example.com/posts", JSON.stringify({
title: randomItem(titles),
}), {
headers: { "Content-Type": "application/json" },
});
const postId = createRes.json("id");
const detail = http.get(`https://api.example.com/posts/${postId}`);
check(detail, { "detail sesuai id yang dibuat": (r) => r.status === 200 });
}This combination of two techniques is what makes the load feel alive: input is picked at random from available data, then the system's output is reused as the next input. Note the order — randomItem(titles) gives input variation, postId ensures the second request always targets a resource that actually exists, because it was just created. A script using a hardcoded id will start failing when the test data no longer contains that id.
This is the part that's least discussed yet most often ruins test results. Non-unique test data is the culprit: when a hundred VUs register with the same email, only the first account succeeds and the rest receive errors — then you blame the application when it's the data that's a mess. The solution: generate unique values programmatically:
export default function () {
const email = `user-${__VU}-${__ITERATION}@example.com`;
const payload = JSON.stringify({
email,
nama: randomItem(["Arif", "Dewi", "Bima"]),
});
http.post("https://api.example.com/register", payload, {
headers: { "Content-Type": "application/json" },
});
}__VU and __ITERATION are built-in k6 variables that guarantee uniqueness: their combination never repeats. This is one pattern for keeping data clean; round it out with these practices:
teardown or by a cleanup job — otherwise the next run starts with the debris of the previous one.Warning
Real credential data must not go into data files that end up in version control. Always use synthetic test accounts for load tests, and keep data files containing secrets out of git — preferably injected through a CI secrets mechanism as discussed in episode 16.
Tie everything together — credential data from CSV, random selection, and the auth module from episode 6 — into a realistic, repeatable scenario:
import http from "k6/http";
import { check } from "k6";
import papaparse from "https://jslib.k6.io/papaparse/5.1/k6-parser.js";
import { SharedArray } from "k6/data";
import { randomItem } from "https://jslib.k6.io/k6-utils/1.6.0/index.js";
const users = new SharedArray("users", function () {
return papaparse.parse(open("./users.csv"), { header: true }).data;
});
export const options = {
vus: 10,
duration: "1m",
};
export default function () {
const user = randomItem(users);
const login = http.post("https://api.example.com/login", JSON.stringify({
email: user.email,
password: user.password,
}), {
headers: { "Content-Type": "application/json" },
});
check(login, {
"login dengan data CSV sukses": (r) => r.status === 200,
});
}Notice the whole data chain: open reads the file, papaparse parses it, SharedArray shares it, randomItem selects from it, and the result becomes the login payload. Not a single hardcoded value in the scenario. The next run with the same CSV file stays consistent — and with a new CSV, the test changes without touching code.
SharedArray.SharedArray.header: true in papaparse. The result becomes an array of arrays — brittle and hard to read.In this episode 8 you've made data part of your load test arsenal:
SharedArray from k6/data: large data shared across VUs without multiplying memory.papaparse from jslib: CSV becomes structured objects thanks to the header: true option.randomItem from k6-utils: expressive, correlatable random input.Save the complete scenario as ep8-data.js, run it with k6 run ep8-data.js, and observe login results from random CSV data. Your scripts are now data-powered, but still running with a single simple load model. In episode 9 we enter a round that changes everything — Complex Scenarios and Multi-endpoint Workloads: composing average, spike, and soak load mixes in one script, separating traffic with group(), and measuring things specific to your application with the Trend and Rate custom metrics. See you in episode 9!