Mengekstrak nilai dinamis dari respons seperti token akses dan CSRF token, mengelola cookie jar dan state sesi per virtual user, serta memakai http.batch untuk request paralel yang efisien.

Di episode 4 kalian menulis request HTTP yang realistis: header bearer token, cookies, payload JSON, dan dua lapis kendali mutu. Tapi ada satu hal yang masih kita anggap statis: nilai-nilai seperti token dan cookie yang kalian tulis manual. Aplikasi sungguhan tidak bekerja begitu.
Bayangkan masuk ke sebuah gedung. Episode 4 melatih kalian menunjukkan kartu akses; episode 5 mengajarkan cara memperoleh kartu itu sendiri — dan mengapa kartu yang sama tidak boleh dipakai semua orang. Tiga kemampuan inti yang akan dikuasai: menyimpan nilai dinamis dari respons (token, CSRF, cookie), mengelola sesi di dalam setiap virtual user, dan melakukan banyak request paralel dengan http.batch(). Skrip di akhir episode adalah beban yang jauh lebih mirip pengguna asli.
Alur paling sering muncul di load test adalah login dulu, baru panggil API — token tidak bisa di-hardcode, ia harus diambil dari respons login saat test berjalan. k6 menyediakan res.json() untuk mem-parse body JSON, dan varian res.json("access_token") untuk langsung mengambil satu field:
import http from "k6/http";
import { check } from "k6";
export default function () {
const res = http.post("https://api.example.com/login", JSON.stringify({
email: "user@example.com",
password: "rahasia123",
}), {
headers: { "Content-Type": "application/json" },
});
const token = res.json("access_token");
check(res, {
"login sukses": (r) => r.status === 200,
"token tersedia": (r) => typeof res.json("access_token") === "string",
});
}Dua bentuk res.json() yang perlu kalian kenali. res.json("access_token") mengambil satu field dengan selector — selector bisa berupa lintasan titik seperti res.json("data.access_token") untuk objek bertingkat. Sedangkan res.json().access_token mem-parse seluruh body lalu mengakses field lewat sintaks objek JavaScript — lebih fleksibel saat kalian butuh beberapa field sekaligus. Pilih salah satu dan gunakan konsisten; konsistensi membuat skrip mudah dibaca rekan kerja. Pola reuse-nya — token hasil ekstraksi dipakai di request berikutnya — akan kalian lihat utuh di skenario lengkap akhir episode.
Tidak semua token datang dari JSON. Aplikasi web klasik menyisipkan CSRF token di halaman form — nilainya dinamis, di-generate per kunjungan, dan wajib dikirim ulang saat form di-submit. Menghardcode nilai ini adalah kesalahan fatal: iterasi kedua akan gagal karena token sudah basi. k6 menyediakan res.html() yang mengembalikan objek HTML ber-API ala jQuery — di-query dengan .find() dan atributnya dibaca dengan .attr():
import http from "k6/http";
export default function () {
const page = http.get("https://api.example.com/transfer");
const csrf = page.html()
.find('meta[name="csrf-token"]')
.attr("content");
const res = http.post("https://api.example.com/transfer", JSON.stringify({
amount: 50000,
}), {
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrf,
},
});
}Pola "ambil dari halaman, kirim kembali" adalah inti token dance — urutan pertukaran nilai dinamis yang wajib ditiru persis seperti browser. Jika CSRF kalian disimpan di hidden input, ganti selector menjadi .find('input[name="csrf_token"]').attr("value"). Kuncinya: tanyakan selalu dari mana browser mendapat nilai ini, lalu tiru alurnya.
Tip
Parsing HTML di k6 memakai selector CSS. Untuk token yang disisipkan di dalam JavaScript inline atau nilai yang dibangun dinamis, regex sering jadi jalan keluar yang lebih andal. Pastikan nilai yang diekstrak identik dengan yang dikirim browser sebelum kalian mempercayai hasil test.
Di episode 4 kalian tahu cookie jar k6 bersifat per VU — setiap virtual user punya gudang cookie sendiri, seperti pengguna browser berbeda. Episode ini menambahkan kendali eksplisit lewat http.cookieJar():
import http from "k6/http";
export const options = {
cookies: {
locale: "id-ID",
},
};
export default function () {
const jar = http.cookieJar();
jar.set("https://api.example.com", "theme", "dark");
const cookies = jar.cookiesForURL("https://api.example.com");
console.log("Cookie yang tersimpan:", Object.keys(cookies));
http.get("https://api.example.com/home");
}Tiga hal yang terjadi: options.cookies menabur cookie default untuk semua VU sejak lahir; jar.set(url, nama, nilai) menulis cookie ke jar VU aktif; dan jar.cookiesForURL(url) membaca semua cookie yang berlaku untuk sebuah URL — sehingga kalian bisa memastikan cookie sesi terpasang sebelum request penting. Kenapa perlu kendali eksplisit padahal k6 sudah otomatis? Karena ada skenario yang tidak cukup: cookie yang harus ditebak dari respons, cookie yang diputar di tengah test, atau sesi yang ingin direset untuk mensimulasikan pengguna baru. Cookie jar adalah state — dan state adalah hal yang paling sering membedakan skrip amatir dari skrip profesional.
Pertanyaan yang tadi kita tunda: haruskah login setiap iterasi? Pola yang paling sering benar adalah login sekali, pakai terus dalam satu VU — mengukur /login padahal targetnya /profile hanya menambah noise. Masalahnya, variabel di dalam fungsi default di-reset setiap iterasi. Solusinya: simpan state di luar fungsi, dengan kunci __VU agar setiap VU punya slot sendiri:
import http from "k6/http";
const sessions = {};
export default function () {
if (!sessions[__VU]) {
sessions[__VU] = { token: null };
}
if (!sessions[__VU].token) {
const login = http.post("https://api.example.com/login", JSON.stringify({
email: "user@example.com",
password: "rahasia123",
}), {
headers: { "Content-Type": "application/json" },
});
sessions[__VU].token = login.json("access_token");
}
http.get("https://api.example.com/profile", {
headers: {
Authorization: `Bearer ${sessions[__VU].token}`,
},
});
}Pola ini penting dan sering disalahpahami. Variabel tingkat modul di k6 dibagi antar semua VU — menyimpan token di satu variabel global berarti VU-1 bisa menimpa token VU-2. Dengan objek yang dikunci __VU, setiap virtual user punya sesi mandiri yang bertahan sepanjang hidupnya — simulasi paling akurat untuk pengguna yang login sekali lalu beraktivitas. Pola ini akan kembali muncul lebih rapi di episode 6 (reusable function) dan episode 8 (data per user).
Satu halaman web modern bisa memicu belasan request API sekaligus. Menuliskannya berurutan membuat VU menunggu tiap respons satu per satu — bukan perilaku browser, dan durasi test membengkak. http.batch() mengirim beberapa request secara paralel dalam satu panggilan:
import http from "k6/http";
import { check } from "k6";
export default function () {
const token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhcm1hbiJ9.tanda-tangan";
const responses = http.batch([
{ method: "GET", url: "https://api.example.com/profile" },
{ method: "GET", url: "https://api.example.com/orders" },
{
method: "GET",
url: "https://api.example.com/cart",
headers: { Authorization: `Bearer ${token}` },
},
]);
check(responses[0], { "profil 200": (r) => r.status === 200 });
check(responses[1], { "orders 200": (r) => r.status === 200 });
check(responses[2], { "cart 200": (r) => r.status === 200 });
}batch menerima array objek request (method, url, body, headers) atau pasangan tuple, dan mengembalikan array response yang urutannya sama dengan input — jadi responses[0] adalah balasan request pertama. Semua request dikirim bersamaan, diukur sebagai satu gelombang, namun tetap tercatat sebagai metrik HTTP terpisah dengan tag bawaan url. Satu jebakan: token tidak otomatis disisipkan di dalam batch — kirim header Authorization secara eksplisit per request, persis contoh di atas.
Rangkai semua lapisan menjadi satu skenario yang meniru perjalanan pengguna: login sekali (state per VU), lalu memuat halaman yang menembak beberapa API paralel:
import http from "k6/http";
import { check } from "k6";
const sessions = {};
export const options = {
vus: 20,
duration: "1m",
thresholds: {
http_req_duration: ["p(95)<500"],
},
};
export default function () {
if (!sessions[__VU]) {
const login = http.post("https://api.example.com/login", JSON.stringify({
email: "user@example.com",
password: "rahasia123",
}), {
headers: { "Content-Type": "application/json" },
});
sessions[__VU] = { token: login.json("access_token") };
}
const token = sessions[__VU].token;
const responses = http.batch([
{ method: "GET", url: "https://api.example.com/dashboard", headers: { Authorization: `Bearer ${token}` } },
{ method: "GET", url: "https://api.example.com/notifications", headers: { Authorization: `Bearer ${token}` } },
{ method: "GET", url: "https://api.example.com/activities", headers: { Authorization: `Bearer ${token}` } },
]);
check(responses, {
"semua endpoint 200": (rs) => rs.every((r) => r.status === 200),
"dashboard cepat": (rs) => rs[0].timings.duration < 400,
});
}Perhatikan rs.every(...) — check bisa membaca seluruh array respons sekaligus. Skenario ini mengukur beban halaman seutuhnya: login jarang, request paralel dominan, sesi per VU terjaga. Inilah perbedaan antara mengukur endpoint dan mengukur pengalaman pengguna.
__VU. VU saling menimpa token, sesi jadi silang, hasil test jadi sampah./login berulang padahal targetnya endpoint lain membuat beban tidak proporsional.http.get berurutan untuk request yang sebenarnya paralel. Simulasi lebih lambat dari kenyataan dan durasi test membengkak.Authorization di dalam http.batch(). Setiap request batch butuh header sendiri — tidak ada pewarisan otomatis.Di episode 5 ini kalian naik satu level dari "mengirim request" ke "mensimulasikan pengguna":
res.json() dan res.json("access_token") untuk token dari JSON, res.html().find(...).attr(...) untuk CSRF dari HTML.options.cookies untuk taburan default, http.cookieJar() untuk menulis dan membaca cookie per VU.__VU agar login cukup sekali dan sesi bertahan lintas iterasi.http.batch() untuk meniru beban halaman web modern dan memangkas durasi test.Simpan contoh terakhir sebagai ep5-session.js dan jalankan dengan k6 run ep5-session.js. Namun satu masalah mulai terasa: file skrip semakin panjang dan banyak yang berulang. Di episode 6 kita membereskan itu — Modularisasi Skrip & Reusable Function: memecah skrip menjadi beberapa file, membangun fungsi helper untuk request dan auth, dan merancang skenario yang bisa dipakai ulang. Sampai jumpa di episode 6!