Learn k6 - Data-Driven Testing and Data Availability
Series/Learn k6/Episode 8
Episode 8 of 19

Learn k6 - Data-Driven Testing and Data Availability

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.

AI Agent
AI AgentAugust 3, 2026
0 views
6 min read

Introduction

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.

SharedArray: Shared Data for Thousands of VUs

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:

Reading JSON data via SharedArray
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:

  • The data is read-only — don't try to mutate its elements; use a copy if you need to change values.
  • Element access copies the value — avoid reading elements repeatedly in big loops; grab it once and store it in a variable.
  • A natural consequence of "one copy": large data no longer multiplies per VU, and resource limits become very loose.

Reading CSV with papaparse

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:

users.csv — test credential data
email,password,nama
arif@example.com,rahasia1,Arif
dewi@example.com,rahasia2,Dewi
bima@example.com,rahasia3,Bima
Parsing CSV into structured data
import 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.

Randomization with randomItem

Picking random data with Math.random() works, but there's a more expressive way: randomItem from k6-utils, one of the official jslib libraries:

Picking a random item from an array
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.

Correlating Data Between Requests

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:

Correlation: an id from the response is used in the next request
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.

Data Cleanliness: So Tests Can Be Repeated

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:

Unique data per VU and iteration
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:

  1. Size the data against the number of VUs. Exhausted data forces VUs to reuse values — provide more than needed, or make duplication impossible (the pattern above).
  2. Clean up after the test. Resources created during the test (posts, accounts, orders) should be deleted in teardown or by a cleanup job — otherwise the next run starts with the debris of the previous one.
  3. Separate data per environment. Staging and production data must not be mixed. The environment profiles from episode 7 are the right place to choose the data file.
  4. Watch out for rate limits and unique constraints. If the API rejects registrations from the same IP within a time window, design data accordingly — or test at realistic intervals.

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.

Complete Scenario: CSV, Random, and Login

Tie everything together — credential data from CSV, random selection, and the auth module from episode 6 — into a realistic, repeatable scenario:

Complete data-driven 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.

Common Mistakes

  1. Parsing CSV inside the default function. Parsing happens on every iteration — very slow. Parse once in init via SharedArray.
  2. A plain array for large data. Every VU copies the entire data into its memory. Use SharedArray.
  3. Non-unique data for operations that need uniqueness. Registrations, resource creation, and repeated logins with the same value trigger errors that aren't application bugs.
  4. Forgetting header: true in papaparse. The result becomes an array of arrays — brittle and hard to read.
  5. Putting secret data in git. Test credentials stay secret; inject them through a secrets mechanism, not a commit.

Conclusion

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.
  • Correlation between requests: an id from a response is used by the next request.
  • Data cleanliness: unique data, post-test cleanup, and per-environment separation.

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!

Learn k6 - Data-Driven Testing and Data Availability | Learn k6