API Testing
Tes REST API lewat studi kasus nyata di tiga proyek (semuanya Bearer token), kontrak OpenAPI dan contract test, hybrid testing, dan network mocking.
UI test meniru pengguna tapi lambat. API test langsung bicara ke server, cepat dan stabil. Gabungan keduanya disebut hybrid testing: siapkan data lewat API, lalu cek pengalaman pengguna lewat UI.
Seperti mengetes lewat Postman, tapi otomatis dan bisa diulang. Hybrid testing seperti menyiapkan data lewat back office, lalu mengecek tampilannya dari sisi pengguna.
Kenalan singkat dengan HTTP dan REST
Setiap request punya method (jenis permintaan), dan server menjawab dengan status code yang menunjukkan hasilnya.
| Method | Gunanya | Contoh di TokoQA |
|---|---|---|
GET | Mengambil data | GET /api/products |
POST | Mengirim atau membuat data baru | POST /api/login, POST /api/cart |
PUT | Mengganti seluruh data | PUT /api/addresses/1 |
PATCH | Mengubah sebagian data | PATCH /api/addresses/1 |
DELETE | Menghapus data | DELETE /api/cart/2 |
| Status | Artinya |
|---|---|
200 OK / 201 Created / 204 No Content | Berhasil / berhasil membuat data baru / berhasil, tanpa isi balasan |
400 Bad Request | Permintaannya tidak sesuai format |
401 Unauthorized | Belum login, atau token-nya salah |
403 Forbidden | Sudah dikenali, tapi tidak punya izin |
404 Not Found | Data atau URL-nya tidak ada |
409 Conflict | Bentrok dengan kondisi data (misalnya stok habis) |
410 Gone | Dulu ada, sekarang sudah tidak berlaku |
422 Unprocessable Content | Formatnya benar, tapi isinya tidak valid |
429 Too Many Requests | Terlalu banyak percobaan, tunggu sesuai Retry-After |
500 Server Error | Ada yang rusak di server, hampir selalu bug |
API TokoQA yang bisa kamu tes
Daftar lengkap dan tempat mencobanya ada di API Lab. Ringkasnya:
| Endpoint | Butuh token? | Keterangan |
|---|---|---|
POST /api/login | Tidak | body {username, password} → {token, user}. Opsional ttl (detik) |
GET /api/products | Tidak | opsional q, category, sort, page, limit |
GET /api/products/:id | Tidak | detail produk atau 404 |
GET /api/me | Ya | profil user dari token |
GET · POST · DELETE /api/cart | Ya, customer | keranjang milik sesi token |
DELETE /api/cart/:productId | Ya, customer | hapus satu item |
POST /api/coupons/apply | Ya, customer | cek kupon, maksimal 5 percobaan per menit |
GET · POST /api/orders, GET /api/orders/:id | Ya, customer | riwayat, buat pesanan (kupon opsional), detail beserta status |
GET · POST /api/addresses, GET · PUT · PATCH · DELETE /api/addresses/:id | Ya, customer | tambah, lihat, ubah, hapus alamat pengiriman |
GET /api/admin/orders, GET /api/admin/summary | Ya, admin | pesanan semua customer dan ringkasan toko |
PATCH /api/admin/orders/:id | Ya, admin | ubah status: Baru, Diproses, Dikirim, Selesai, Dibatalkan |
GET /api/slow?ms=2000 | Tidak | sengaja dibuat lambat, untuk latihan timeout |
Endpoint belanja menolak token admin dengan 403, dan endpoint admin menolak token customer dengan 403. Buka /api/products di browser untuk melihat data JSON-nya.
Request pertama
def test_get_semua_produk(playwright, base_url): # playwright dan base_url adalah fixture bawaan
api = playwright.request.new_context(base_url=base_url) # klien API tanpa browser
res = api.get("/api/products") # GET daftar produk
expect(res).to_be_ok() # status 2xx
body = res.json() # isi respons
assert body["total"] == 8 # ada 8 produk
api.dispose() # tutup klien APItest('GET semua produk', async ({ request }) => { // request = klien API bawaan Playwright Test
// fixture `request` otomatis memakai baseURL dari config
const res = await request.get('/api/products');
await expect(res).toBeOK(); // status 2xx
const body = await res.json(); // isi respons
expect(body.total).toBe(8); // ada 8 produk
});Supaya tidak perlu menulis new_context dan dispose di setiap test, bungkus jadi fixture dengan yield (lihat fixture api di file asli di bawah).
POST, query params, dan header token
# Query string: /api/products?q=mouse
res = api.get("/api/products", params={"q": "mouse"})
# POST JSON: dict otomatis dikirim sebagai JSON
res = api.post("/api/login", data={"username": "customer", "password": "rahasia123"})
token = res.json()["token"] # token dari respons login
# Endpoint yang butuh login: kirim token di header Authorization
res = api.post(
"/api/cart",
data={"productId": 2, "qty": 2},
headers={"Authorization": f"Bearer {token}"},
)
assert res.status == 201 # dibuat (201)// Query string: /api/products?q=mouse
let res = await request.get('/api/products', { params: { q: 'mouse' } });
// POST JSON: object otomatis dikirim sebagai JSON
res = await request.post('/api/login', { data: { username: 'customer', password: 'rahasia123' } });
const { token } = await res.json(); // token dari respons login
// Endpoint yang butuh login: kirim token di header Authorization
res = await request.post('/api/cart', {
data: { productId: 2, qty: 2 },
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status()).toBe(201); // dibuat (201)Token dari /api/login berformat JWT: header.payload.signature. Bagian payload berisi username dan waktu kedaluwarsa, dan bisa dilihat di jwt.io. Jangan tempel token dari aplikasi sungguhan ke situs mana pun.
Validasi bentuk respons dengan JSON Schema
Mengecek satu atau dua field belum cukup, misalnya kalau price tiba-tiba dikirim sebagai teks. JSON Schema menuliskan bentuk data yang benar, lalu library mengecek apakah respons cocok.
from jsonschema import validate # uv add jsonschema
PRODUCT_SCHEMA = { # bentuk produk yang diharapkan (JSON Schema)
"type": "object",
"required": ["id", "name", "category", "price", "stock"],
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
"category": {"enum": ["Elektronik", "Furnitur", "Aksesoris"]},
"price": {"type": "integer", "minimum": 0},
"stock": {"type": "integer", "minimum": 0},
},
}
validate(instance=res.json(), schema=PRODUCT_SCHEMA) # error jelas kalau tidak cocokimport { Ajv } from 'ajv'; // npm i -D ajv
const productSchema = { // bentuk produk yang diharapkan (JSON Schema)
type: 'object',
required: ['id', 'name', 'category', 'price', 'stock'],
properties: {
id: { type: 'integer' },
name: { type: 'string' },
category: { enum: ['Elektronik', 'Furnitur', 'Aksesoris'] },
price: { type: 'integer', minimum: 0 },
stock: { type: 'integer', minimum: 0 },
},
};
const validate = new Ajv().compile(productSchema); // siapkan validator dari schema
expect(validate(await res.json()), JSON.stringify(validate.errors)).toBe(true); // cocok dengan schema, kalau gagal tampilkan error-nyaFile asli: API testing
Memuat tests/academy/test_m13_api.py…
uv run pytest tests/academy/test_m13_api.py -vMemuat tests/academy/m13-api.spec.ts…
npx playwright test m13-api --project=chromiumHybrid testing: API + UI
Tiga cara gabungan yang paling sering dipakai:
| Cara | Contoh | Untungnya |
|---|---|---|
| Login lewat API | Ambil token, lalu pasang ke browser | Hemat beberapa detik per test, dan tidak bergantung pada form login |
| Siapkan data lewat API | Isi keranjang lewat API, lalu cek tampilannya di UI | Tahap persiapan jadi cepat dan stabil |
| Cek hasil lewat API | Checkout lewat UI, lalu cek pesanannya lewat API | Memastikan data benar-benar tersimpan |
TokoQA menyimpan token di localStorage dengan kunci tokoqa_token. Untuk melewati form login, taruh token di sana sebelum halaman dimuat dengan add_init_script / addInitScript.
token = api.post("/api/login", data={...}).json()["token"] # login lewat API
page.add_init_script(f"window.localStorage.setItem('tokoqa_token', '{token}')") # simpan token sebelum halaman dimuat
page.goto("/shop.html") # langsung masuk, tanpa form login!
expect(page.get_by_test_id("greeting")).to_have_text("Halo, Rina Wulandari") # browser sudah login tanpa formconst { token } = await (await request.post('/api/login', { data: { /* ... */ } })).json(); // login lewat API
await page.addInitScript((t) => window.localStorage.setItem('tokoqa_token', t), token); // simpan token sebelum halaman dimuat
await page.goto('/shop.html'); // langsung masuk, tanpa form login!
await expect(page.getByTestId('greeting')).toHaveText('Halo, Rina Wulandari'); // browser sudah login tanpa form- Aplikasi yang menyimpan sesi di cookie (bukan di localStorage) memakai
context.add_cookies([...])/context.addCookies([...]). - Tetap sisakan minimal satu test smoke untuk alur login lewat UI.
Network mocking: mengendalikan respons server
page.route meng-intercept request dari browser lalu membalasnya sendiri. Gunanya:
- Menguji tampilan saat server error atau datanya kosong, yang susah dibuat di server asli.
- Tetap bisa menguji tampilan walaupun backend belum siap atau sedang tidak stabil.
- Memblokir hal yang tidak perlu (iklan, analytics) supaya test lebih cepat.
# Balas dengan data mock
page.route("**/api/products", lambda route: route.fulfill(
json={"total": 1, "data": [{"id": 99, "name": "Produk Palsu"}]}
))
# Simulasi server error
page.route("**/api/products", lambda route: route.fulfill(status=500, json={"error": "boom"}))
# Blokir gambar
page.route("**/*.{png,jpg,jpeg,webp}", lambda route: route.abort())
# Ubah sebagian respons asli
def diskon(route):
response = route.fetch() # teruskan request ke server sungguhan
body = response.json() # isi respons asli
body["data"][0]["price"] = 1 # ubah harga produk pertama
route.fulfill(response=response, json=body) # kirim respons yang sudah diubah ke halaman
page.route("**/api/products", diskon) # pasang pencegat untuk /api/products// Balas dengan data mock
await page.route('**/api/products', (route) =>
route.fulfill({ json: { total: 1, data: [{ id: 99, name: 'Produk Palsu' }] } }), // balas dengan data palsu
);
// Simulasi server error
await page.route('**/api/products', (route) => route.fulfill({ status: 500, json: { error: 'boom' } }));
// Blokir gambar
await page.route('**/*.{png,jpg,jpeg,webp}', (route) => route.abort());
// Ubah sebagian respons asli
await page.route('**/api/products', async (route) => {
const response = await route.fetch();
const body = await response.json(); // isi respons asli
body.data[0].price = 1; // ubah harga produk pertama
await route.fulfill({ response, json: body }); // kirim respons yang sudah diubah ke halaman
});File asli: hybrid & mocking
Memuat tests/academy/test_m13_hybrid.py…
uv run pytest tests/academy/test_m13_hybrid.py -vMemuat tests/academy/m13-hybrid.spec.ts…
npx playwright test m13-hybrid --project=chromiumStudi kasus API
Tujuh situasi yang sering ditemui di pekerjaan nyata. Buka satu per satu, coba manual di API Lab, lalu lihat versi otomatisnya di file test di akhir bagian ini.
Untuk setiap endpoint, tanyakan empat hal: kalau semua benar, kalau datanya salah, kalau tidak punya izin, dan kalau sampai di batas (halaman terakhir, stok habis, batas percobaan).
Studi kasus 1: CRUD alamat pengiriman201 · 200 · 204 · 404
Pengguna bisa menyimpan sampai 5 alamat. Kita uji satu data dari lahir sampai dihapus: Create (buat), Read (baca), Update (ubah), Delete (hapus).
| Langkah | Request | Yang dicek |
|---|---|---|
| Buat | POST /api/addresses | 201, ada header Location, alamat pertama otomatis jadi alamat utama |
| Baca | GET /api/addresses/{id} | datanya sama dengan yang dikirim |
| Ganti semua | PUT /api/addresses/{id} | semua field wajib dikirim, hasilnya 200 |
| Ubah sebagian | PATCH /api/addresses/{id} | hanya field yang dikirim yang berubah |
| Hapus | DELETE /api/addresses/{id} | 204 tanpa isi, lalu GET jadi 404 |
res = api.post("/api/addresses", data=ALAMAT_RUMAH, headers=auth) # Create
assert res.status == 201 # dibuat (201)
alamat = Address.model_validate(res.json()) # Pydantic: validasi + objek bertipe
assert res.headers["location"] == f"/api/addresses/{alamat.id}" # header Location menunjuk alamat baru
res = api.patch(f"/api/addresses/{alamat.id}", data={"label": "Rumah Ibu"}, headers=auth) # Patch: ubah sebagian
assert res.json()["label"] == "Rumah Ibu" # labelnya berubah
assert api.delete(f"/api/addresses/{alamat.id}", headers=auth).status == 204 # Delete: 204
assert api.get(f"/api/addresses/{alamat.id}", headers=auth).status == 404 # sesudah dihapus: 404let res = await request.post('/api/addresses', { data: ALAMAT_RUMAH, headers: auth }); // Create
expect(res.status()).toBe(201); // dibuat (201)
const alamat = Address.parse(await res.json()); // Zod: validasi + tipe TypeScript
expect(res.headers()['location']).toBe(`/api/addresses/${alamat.id}`); // header Location menunjuk alamat baru
res = await request.patch(`/api/addresses/${alamat.id}`, { data: { label: 'Rumah Ibu' }, headers: auth }); // Patch: ubah sebagian
expect((await res.json()).label).toBe('Rumah Ibu'); // labelnya berubah
expect((await request.delete(`/api/addresses/${alamat.id}`, { headers: auth })).status()).toBe(204); // Delete: 204
expect((await request.get(`/api/addresses/${alamat.id}`, { headers: auth })).status()).toBe(404); // sesudah dihapus: 404PUT mengganti seluruh data, jadi field yang tidak dikirim dianggap hilang (di TokoQA ditolak dengan 422). PATCH hanya mengubah field yang dikirim.
Studi kasus 2: Validasi input dan pesan error422 · 409
API yang baik menolak data yang salah dengan status yang tepat, dan memberi tahu field mana yang salah. TokoQA membalas dengan 422 plus objek fields.
| Data yang dikirim | Status | fields berisi |
|---|---|---|
postalCode: "4011" (4 digit) | 422 | postalCode |
phone: "12345" (tidak diawali 08) | 422 | phone |
label: "" (kosong) | 422 | label |
field tidak dikenal, misalnya warna | 422 | warna |
| alamat ke-6 | 409 | (pesan: maksimal 5 alamat) |
@pytest.mark.parametrize(("ubah", "field_error"), [ # jalankan dengan banyak data salah
({"postalCode": "4011"}, "postalCode"),
({"phone": "12345"}, "phone"),
])
def test_validasi_alamat_422(api, auth, ubah, field_error): # satu test per baris data
res = api.post("/api/addresses", data={**ALAMAT_RUMAH, **ubah}, headers=auth) # kirim alamat dengan satu field yang salah
assert res.status == 422 # ditolak 422
assert list(res.json()["fields"]) == [field_error] # hanya field yang salahfor (const { judul, ubah, field } of kasus) { // satu test per kasus
test(`422: ${judul}`, async ({ request }) => {
const res = await request.post('/api/addresses', { data: { ...ALAMAT_RUMAH, ...ubah }, headers: await login(request) });
expect(res.status()).toBe(422); // ditolak 422
expect(Object.keys((await res.json()).fields)).toEqual([field]); // hanya field yang salah
});
}Studi kasus 3: Pagination dan sorting200 · 400
Endpoint daftar mengirim data per halaman. Bug yang sering lolos: data muncul dua kali atau terlewat di pergantian halaman, dan urutannya berubah-ubah.
| Skenario | Request | Expected |
|---|---|---|
| Halaman pertama | GET /api/products?page=1&limit=3 | total: 8, totalPages: 3, 3 item |
| Kumpulkan semua halaman | loop page = 1..totalPages | 8 id, tidak ada yang dobel |
| Urutkan harga | GET /api/products?sort=price_asc | harga selalu naik |
| Parameter salah | sort=termahal, page=0, limit=100 | 400 |
first = api.get("/api/products", params={"page": 1, "limit": 3}).json() # halaman pertama, 3 per halaman
semua = [] # kumpulkan semua produk
for page in range(1, first["totalPages"] + 1): # halaman 1 sampai terakhir
semua += api.get("/api/products", params={"page": page, "limit": 3}).json()["data"] # tambahkan isi halaman itu
ids = [p["id"] for p in semua] # nomor id semua produk
assert len(ids) == len(set(ids)) == 8 # tidak dobel, tidak terlewatconst first = await (await request.get('/api/products', { params: { page: 1, limit: 3 } })).json(); // halaman pertama, 3 per halaman
const semua: { id: number }[] = []; // kumpulkan semua produk
for (let page = 1; page <= first.totalPages; page++) { // halaman 1 sampai terakhir
semua.push(...(await (await request.get('/api/products', { params: { page, limit: 3 } })).json()).data);
}
expect(new Set(semua.map((p) => p.id)).size).toBe(8); // tidak dobel, tidak terlewatStudi kasus 4: Autentikasi dan token kedaluwarsa401 · 403
Ada tiga alasan berbeda yang sama-sama menghasilkan 401, dan aplikasi yang baik bisa membedakannya. TokoQA memberi tahu alasannya lewat field reason.
| Kondisi | Cara membuatnya | reason |
|---|---|---|
| Tanpa token | tidak kirim header Authorization | missing |
| Token diubah | ganti 2 karakter terakhir token | invalid |
| Token kedaluwarsa | login dengan ttl: 0 (fitur latihan) | expired |
kedaluwarsa = login(api, ttl=0) # token langsung kedaluwarsa, tanpa menunggu
res = api.get("/api/me", headers=kedaluwarsa) # token kedaluwarsa
assert res.status == 401 # ditolak 401
assert res.json()["reason"] == "expired" # alasannya expiredconst kedaluwarsa = await login(request, 'customer', 'rahasia123', { ttl: 0 }); // tanpa menunggu
const res = await request.get('/api/me', { headers: kedaluwarsa }); // token kedaluwarsa
expect(res.status()).toBe(401); // ditolak 401
expect((await res.json()).reason).toBe('expired'); // alasannya expiredJangan menunggu token kedaluwarsa sungguhan. Minta developer menyediakan jalan pintas di server test, misalnya parameter TTL seperti di TokoQA, atau kendalikan waktu dengan page.clock di sisi UI.
Lanjutan: otorisasi berdasarkan role (401 vs 403)
Autentikasi menjawab "kamu siapa?", otorisasi menjawab "kamu boleh apa?". Di TokoQA, admin boleh membuka /api/admin/orders, sedangkan customer sudah login tapi tidak punya akses.
| Kondisi | Status | Artinya |
|---|---|---|
Buka /api/admin/orders tanpa token | 401 | Belum login |
Buka dengan token customer | 403 | Sudah login, tapi role-nya tidak punya akses |
Buka dengan token admin | 200 | Punya akses, dan mendapat pesanan semua akun |
assert api.get("/api/admin/orders").status == 401 # tanpa token: 401
res = api.get("/api/admin/orders", headers=auth) # auth = login sebagai customer
assert res.status == 403 # customer: 403
semua = api.get("/api/admin/orders", headers=login(api, "admin", "admin123")).json()["data"] # admin bisa melihat semua pesananexpect((await request.get('/api/admin/orders')).status()).toBe(401); // tanpa token: 401
const res = await request.get('/api/admin/orders', { headers: await login(request) }); // customer
expect(res.status()).toBe(403); // customer: 403
const { data } = await (await request.get('/api/admin/orders', { headers: await login(request, 'admin', 'admin123') })).json(); // admin bisa melihat semua pesananHak akses sebaiknya diuji di API (server menolak) dan di UI (menunya tidak ditampilkan). Test dashboard admin hanya untuk admin di Modul 17 mengecek sisi UI-nya.
Studi kasus 5: Kupon dan rate limit200 · 404 · 410 · 422 · 429
Satu endpoint, banyak kemungkinan hasil. Supaya kode kupon tidak bisa ditebak, percobaan dibatasi 5 kali per menit (rate limit).
| Kode | Kondisi | Status |
|---|---|---|
hemat10 | valid (huruf kecil juga diterima) | 200, diskon 10% |
KADALUARSA | sudah berakhir | 410 Gone |
TIDAKADA | tidak terdaftar | 404 |
HEMAT50K | subtotal di bawah Rp 500.000 | 422 |
| percobaan ke-6 dalam semenit | rate limit | 429 + header Retry-After |
statuses = [api.post("/api/coupons/apply", data={"code": "TIDAKADA"}, headers=auth).status for _ in range(5)] # 5 kali salah berturut-turut
assert statuses == [404] * 5 # semuanya 404
res = api.post("/api/coupons/apply", data={"code": "HEMAT10"}, headers=auth) # percobaan keenam
assert res.status == 429 # ditolak 429
assert int(res.headers["retry-after"]) > 0 # server memberi tahu kapan boleh mencoba lagifor (let i = 0; i < 5; i++) { // 5 kali salah berturut-turut
expect((await request.post('/api/coupons/apply', { data: { code: 'TIDAKADA' }, headers: auth })).status()).toBe(404);
}
const res = await request.post('/api/coupons/apply', { data: { code: 'HEMAT10' }, headers: auth }); // percobaan keenam
expect(res.status()).toBe(429); // ditolak 429
expect(Number(res.headers()['retry-after'])).toBeGreaterThan(0); // header Retry-After: tunggu berapa detikStudi kasus 6: Alur pesanan end-to-end lewat API201 · 404
Rangkai request seperti pengguna sungguhan, tanpa browser. Tekniknya disebut chaining: hasil satu request dipakai di request berikutnya.
- Tambah Keyboard Mekanik (Rp 750.000) dan 2 Lampu Meja LED (2 x Rp 120.000).
- Buat pesanan dengan kupon
HEMAT10, lalu cek subtotal Rp 990.000, diskon Rp 99.000, dan total Rp 891.000. - Ikuti header
Locationuntuk membaca detail pesanan. - Pastikan pesanan muncul di
GET /api/ordersdan keranjang sudah kosong. - Keamanan: sesi lain tidak boleh bisa membaca pesanan ini. Jawabannya
404, bukan403, supaya orang lain bahkan tidak tahu pesanan itu ada.
res = api.post("/api/orders", data={"name": "Rina", "address": "Bandung", "payment": "ewallet", "coupon": "HEMAT10"}, headers=auth) # buat pesanan dengan kupon
order = Order.model_validate(res.json()) # bentuk respons dicek dengan model Pydantic
assert (order.subtotal, order.discount, order.total) == (990_000, 99_000, 891_000) # subtotal, diskon, total
detail = api.get(res.headers["location"], headers=auth) # chaining lewat header Location
assert detail.json()["id"] == order.id # detail pesanan bisa dibuka
assert api.get("/api/cart", headers=auth).json()["items"] == [] # keranjang kosong setelah pesanan dibuatconst res = await request.post('/api/orders', { // buat pesanan dengan kupon
data: { name: 'Rina', address: 'Bandung', payment: 'ewallet', coupon: 'HEMAT10' }, headers: auth,
});
const order = Order.parse(await res.json()); // bentuk respons dicek dengan schema Zod
expect([order.subtotal, order.discount, order.total]).toEqual([990_000, 99_000, 891_000]); // subtotal, diskon, total
const detail = await request.get(res.headers()['location'], { headers: auth }); // chaining
expect((await detail.json()).id).toBe(order.id); // detail pesanan bisa dibukaStudi kasus 7: Timeout dan waktu responstimeout
Endpoint /api/slow?ms=2000 sengaja dibuat lambat. Kita pastikan request yang kelamaan gagal dengan pesan yang jelas, dan respons normal tetap cepat.
with pytest.raises(Error, match="Timeout"): # test lulus kalau request berhenti karena timeout
api.get("/api/slow", params={"ms": 2000}, timeout=500) # server lambat 2 detik, batas tunggu 0,5 detikawait expect(request.get('/api/slow', { params: { ms: 2000 }, timeout: 500 })).rejects.toThrow(/Timeout/); // request harus gagal karena timeoutPengecekan "respons di bawah 500 ms" berguna sebagai alarm awal, tapi bukan load test. Untuk mengukur server saat dipakai banyak orang, pakai k6 atau JMeter.
File lengkap tujuh studi kasus
Memuat tests/academy/test_m13_case_studies.py…
uv run pytest tests/academy/test_m13_case_studies.py -vMemuat tests/academy/m13-case-studies.spec.ts…
npx playwright test m13-case-studies --project=chromiumProyek pasangan lain: KlinikQA dan PerpusQA
Ketiga proyek login dengan cara yang sama: Bearer token (JWT) di header Authorization. Yang berbeda adalah aturan bisnis dan perannya:
| Proyek | Token disimpan di | Yang paling banyak dilatih |
|---|---|---|
| KlinikQA | localStorage klinikqa_token | logout yang mematikan token di server, dua peran (pasien dan dokter), kalender slot dan nomor antrean, batas ubah jadwal, agenda dokter per hari, CRUD catatan pemeriksaan |
| PerpusQA | localStorage perpusqa_token | dua peran (pustakawan dan anggota), CRUD katalog, anggota, dan ulasan, aturan bisnis (batas, stok, blokir), antrean buku yang habis, denda berbasis tanggal dan pelunasannya, alur dua peran API dan UI |
Semua endpoint-nya bisa dicoba di API Lab: pilih proyeknya di bagian atas halaman.
Cara test API memegang login:
- Login lewat endpoint login aplikasinya:
/api/login,/api/klinik/login, atau/api/perpus/login. - Ambil field
tokendari respons. - Kirim di setiap request: header
Authorization: Bearer <token>. Supaya tidak ditulis berulang, pasang sekali lewatextra_http_headers/extraHTTPHeaders, satu request context per peran.
Token satu aplikasi tidak berlaku di aplikasi lain. Token TokoQA yang dikirim ke KlinikQA ditolak 401 dengan reason: "invalid".
KlinikQA 1: Token sesi200 · 401
Pastikan login memberi token, logout benar-benar mematikan token di server, dan token palsu ditolak. Perhatikan field reason: missing berarti tidak ada header Authorization sama sekali, invalid berarti ada token tapi tidak diterima server.
| Kondisi | Status | reason |
|---|---|---|
| Login berhasil | 200 + token dan expiresIn | |
| Token yang sama setelah logout | 401 | invalid |
| Token dikarang sendiri | 401 | invalid |
| Token TokoQA dikirim ke KlinikQA | 401 | invalid |
KlinikQA 2: Slot bentrok dan validasi409 · 422
Jam yang sudah dipesan tidak boleh dipesan lagi (409). Data yang salah ditolak per field (422): dokter tidak dikenal, tanggal hari Minggu (klinik tutup), jam di luar jadwal, dan keluhan yang terlalu pendek.
KlinikQA 3: Hak akses pasien dan dokter403 · 409
Pasien punya CRUD janji temu: membuat, melihat, mengubah jadwal (PATCH berisi date, time, atau complaint), dan membatalkan. Dokter menandai hasil (PATCH berisi status), melihat agenda per hari (GET /api/klinik/agenda) dan data pasien (/appointments/{id}/patient), serta punya CRUD catatan pemeriksaan (/appointments/{id}/notes dan /notes/{id}) yang bisa dibaca pasien. Masing-masing ditolak 403 kalau mencoba pekerjaan peran lain, dan janji temu yang sudah selesai tidak bisa diubah atau dibatalkan (409).
Memuat tests/proyek/test_klinik_api.py…
uv run pytest tests/proyek/test_klinik_api.py -vMemuat tests/proyek/klinik-api.spec.ts…
npx playwright test klinik-api --project=chromiumKlinikQA 4: Antrean, kalender, dan agenda dokter201 · 403 · 409
Aturan bisnis KlinikQA yang paling sering salah di aplikasi sungguhan ada di sini. Nomor antrean mengikuti urutan jam (dr. Sari jam 10:00 = U-05) dan ikut berubah saat jadwal diubah. Jadwal hanya boleh diubah 2 kali. Kalender menandai hari Minggu sebagai tutup dan hari Sabtu drg. Maya sebagai penuh. Dokter melihat agenda per hari lengkap dengan ringkasan jumlah pasien.
| Kondisi | Yang dicek |
|---|---|
| Buat janji jam 10:00 dengan dr. Sari | 201, queueNo = U-05 |
| Ubah jadwal ketiga kalinya | 409 |
| drg. Maya di hari Sabtu | 409, kalender menandai penuh |
| Pasien membuka agenda dokter | 403 |
| Ringkasan agenda | cocok dengan isi agenda di respons yang sama |
Agenda dokter berisi janji dari semua sesi pasien, termasuk test lain yang berjalan paralel. Karena itu test ringkasan tidak menghitung selisih "sebelum dan sesudah", tapi mencocokkan summary dengan isi data di respons yang sama. Teknik ini berguna setiap kali kamu mengetes data yang dipakai bersama.
Memuat tests/proyek/test_klinik_alur.py…
uv run pytest tests/proyek/test_klinik_alur.py -vMemuat tests/proyek/klinik-alur.spec.ts…
npx playwright test klinik-alur --project=chromiumPerpusQA 1: Login, peran, dan isolasi data401 · 403 · 404
Akun demo (pustakawan / pustaka123, anggota / baca123) enak untuk dicoba manual, tapi dipakai bersama. Karena itu setiap test membuka perpustakaannya sendiri dengan username acak, jadi katalog, anggota, dan pinjamannya tidak tercampur dengan test lain. Yang dicek: login memberi token, logout mematikannya, tanpa token dan token salah ditolak dengan reason berbeda, peran yang salah ditolak 403 dengan reason: "role", dan data milik anggota lain atau perpustakaan lain dibalas 404.
| Kondisi | Status | reason |
|---|---|---|
Tanpa header Authorization | 401 | missing |
| Token setelah logout | 401 | invalid |
| Token dikarang sendiri | 401 | invalid |
| Anggota menambah buku, pustakawan meminjam | 403 | role |
| Membaca pinjaman anggota lain | 404 |
PerpusQA 2: CRUD per peran: katalog, anggota, ulasan403 · 409
Setiap peran punya data yang dikelolanya sendiri:
| Peran | CRUD | Aturan yang dicek |
|---|---|---|
| Pustakawan | Buku: POST, GET, PATCH, DELETE /books | judul dan penulis dobel 409, buku yang masih dipinjam tidak boleh dihapus atau dikurangi eksemplarnya (409) |
| Pustakawan | Anggota: POST, GET, PATCH, DELETE /members | username dobel 409, password baru langsung berlaku, anggota yang masih meminjam tidak boleh dihapus (409) |
| Anggota | Ulasan: POST /books/{id}/reviews, GET, PATCH, DELETE /reviews/{id} | satu ulasan per buku (409), ulasan orang lain bisa dibaca tapi tidak bisa diubah (403, reason: "owner"), pustakawan boleh menghapus untuk moderasi |
PerpusQA 3: Aturan peminjaman403 · 409
Batas 3 buku, buku yang sama tidak bisa dipinjam dua kali, dan stok dihitung untuk seluruh perpustakaan: buku dengan 1 eksemplar yang dipinjam Rina tidak bisa dipinjam Budi. Tiga alasan itu sama-sama 409 dengan pesan berbeda. Kartu yang diblokir pustakawan ditolak 403 dengan reason: "blocked", dan perpanjangan hanya boleh sekali, tidak untuk pinjaman yang sudah lewat jatuh tempo.
PerpusQA 4: Denda: dua peran, API dan UI201 · 200
Menunggu 10 hari supaya buku terlambat jelas tidak mungkin. Server latihan menyediakan jalan pintas: field borrowedAt untuk memundurkan tanggal pinjam. Anggota meminjam lewat API dengan tanggal 10 hari lalu, pustakawan masuk lewat UI dan melihat status Terlambat 3 hari, denda Rp 3.000, lalu menerima bukunya lewat modal. Terakhir, cek lewat API bahwa dendanya tercatat di pinjaman, di profil anggota, dan di data anggota milik pustakawan.
Memuat tests/proyek/test_perpus_api.py…
uv run pytest tests/proyek/test_perpus_api.py -vMemuat tests/proyek/perpus-api.spec.ts…
npx playwright test perpus-api --project=chromiumPerpusQA 5: Antrean dan pelunasan denda409
Buku yang habis bisa diantre. Saat eksemplar kembali (atau pustakawan menambah eksemplar), antrean terdepan berubah menjadi Siap diambil dan eksemplarnya disimpan untuk dia, jadi anggota lain tidak bisa menyerobot. Denda yang muncul saat buku diterima tercatat sebagai unpaidFine sampai pustakawan menandainya lunas.
| Kondisi | Yang dicek |
|---|---|
| Antre buku yang masih tersedia | 409 |
| Eksemplar kembali, ada antrean | antrean terdepan Siap diambil, yang lain naik satu posisi |
| Anggota yang masih menunggu mencoba meminjam | 409 berisi posisi antreannya |
| Antrean ketiga | 409 (maksimal 2) |
| Pustakawan melunasi denda | unpaidFine jadi 0, totalFine tetap |
Studi kasus ini melibatkan tiga anggota sekaligus (Rina meminjam, Budi dan Sari antre). Dengan fixture daftar_anggota / daftarAnggota, setiap anggota punya request context sendiri, jadi alurnya tetap pendek.
Memuat tests/proyek/test_perpus_antrean.py…
uv run pytest tests/proyek/test_perpus_antrean.py -vMemuat tests/proyek/perpus-antrean.spec.ts…
npx playwright test perpus-antrean --project=chromiumTokoQA: CRUD kupon oleh admin201 · 409
Customer di TokoQA sudah punya CRUD alamat (Studi kasus 1). Admin juga punya: CRUD kupon promo di /api/admin/coupons. Kupon buatan admin langsung bisa dipakai customer, sedangkan kupon bawaan (HEMAT10, HEMAT50K, KADALUARSA) dikunci (409) supaya test lain yang memakainya tidak terganggu. Kode kupon unik di seluruh toko, jadi test memakai kode acak.
Memuat tests/proyek/test_toko_kupon.py…
uv run pytest tests/proyek/test_toko_kupon.py -vMemuat tests/proyek/toko-kupon.spec.ts…
npx playwright test toko-kupon --project=chromiumTokoQA: Pesanan dari checkout sampai ulasan403 · 409 · 422
Alur pesanan TokoQA punya aturan yang jelas: status hanya bisa maju (Baru, Diproses, Dikirim, Selesai), customer hanya bisa membatalkan selama status masih Baru, ongkir Express Rp 25.000 dan tidak bisa dibayar COD, dan ulasan hanya boleh ditulis setelah pesanan Selesai. Admin juga bisa menambah stok (restock), tapi tidak bisa mengubah harga, karena harga produk dipakai bersama oleh semua test.
| Kondisi | Status |
|---|---|
| Admin memundurkan status Dikirim ke Diproses | 409 |
| Customer membatalkan pesanan yang sudah Diproses | 409 |
| COD dengan pengiriman Express | 422 |
| Ulasan sebelum pesanan Selesai | 403 |
| Restock produk yang dikunci (Tas Laptop) | 409 |
Memuat tests/proyek/test_toko_pesanan.py…
uv run pytest tests/proyek/test_toko_pesanan.py -vMemuat tests/proyek/toko-pesanan.spec.ts…
npx playwright test toko-pesanan --project=chromiumKontrak API dan contract testing
Di tim sungguhan, perilaku API ditulis di kontrak API: endpoint apa saja yang ada, siapa yang boleh memanggil, data apa yang dikirim, dan semua kemungkinan jawabannya.
Formatnya hampir selalu OpenAPI (dulu disebut Swagger). Ketiga proyek latihan punya kontrak lengkap di halaman API Spec, dan file aslinya bisa diunduh: /spec/tokoqa.json, /spec/klinikqa.json, /spec/perpusqa.json.
| Bagian kontrak | Isinya | Jadi test apa |
|---|---|---|
| Peran dan matriks hak akses | Siapa yang boleh memanggil endpoint | Test 401 dan 403 untuk peran yang salah |
| Parameter | Nama, tipe, wajib atau tidak, batas nilai | Test 400 untuk nilai di luar batas |
| Body request | Field wajib dan opsional, aturan tiap field | Test 422 per field |
| Respons per status | Kapan tiap status keluar, header wajib, contoh body | Assert status, header Location, dan isi body |
| Aturan bisnis | Batas, urutan pengecekan, status yang final | Test 409 dan alur bisnis |
| Skema data | Bentuk setiap objek, tanpa field tambahan | Contract test |
Contract test memastikan bentuk respons asli sama dengan kontrak: status-nya terdaftar, header wajibnya ada, field wajibnya lengkap, tipenya benar, dan tidak ada field tambahan. Helper kecil di utils/kontrak mengambil skema yang tepat dari kontrak untuk setiap kombinasi endpoint dan status, jadi test-nya cukup satu baris per request.
from utils.kontrak import muat_kontrak # helper contract test
def test_kontrak_pinjam(api, anggota_api): # anggota_api = klien API anggota yang sudah login
kontrak = muat_kontrak(api, "perpusqa") # unduh kontrak PerpusQA
res = anggota_api.post("/api/perpus/loans", data={"bookId": 13}) # pinjam buku
kontrak.cek("POST", "/api/perpus/loans", res) # 201 terdaftar, header Location ada, body cocok skema Loanimport { muatKontrak } from '../../utils/kontrak'; // helper contract test
test('kontrak pinjam', async ({ request, anggotaApi }) => { // anggotaApi = klien API anggota yang sudah login
const kontrak = await muatKontrak(request, 'perpusqa');
const res = await anggotaApi.post('/api/perpus/loans', { data: { bookId: 13 } }); // pinjam buku
await kontrak.cek('POST', '/api/perpus/loans', res); // 201 terdaftar, header Location ada, body cocok skema Loan
});Contract test menjawab "apakah bentuk jawabannya benar?", test fungsional menjawab "apakah aturannya benar?". Contract test tetap lolos walau denda dihitung salah, jadi suite yang baik punya keduanya.
Memuat tests/proyek/test_kontrak_api.py…
uv run pytest tests/proyek/test_kontrak_api.py -vMemuat tests/proyek/kontrak-api.spec.ts…
npx playwright test kontrak-api --project=chromiumMemuat utils/kontrak.py…
Memuat utils/kontrak.ts…
Lebih jauh (opsional)Trik komunitas
- Zod (TypeScript) dan Pydantic (Python) banyak dipakai sebagai pengganti JSON Schema biasa: sekali tulis, kamu dapat pengecekan data sekaligus tipe datanya. Keduanya dipakai di studi kasus di atas.
- Rekam dan putar ulang HAR:
route_from_har()/routeFromHAR()merekam respons asli sekali, lalu memutarnya lagi saat test. Test UI tetap stabil walaupun backend sedang mati. - Coba dulu manual, baru diotomasi. Coba endpoint di API Lab, Bruno, atau Postman, baru tulis test-nya.
- Contract testing (misalnya Pact) dipakai tim microservice untuk memastikan perubahan API tidak merusak aplikasi lain yang memakainya.
# Rekam sekali (update=True), lalu jalankan ulang tanpa update untuk memakai rekaman
page.route_from_har("data/products.har", url="**/api/products*", update=True)// Rekam sekali (update: true), lalu jalankan ulang tanpa update untuk memakai rekaman
await page.routeFromHAR('data/products.har', { url: '**/api/products*', update: true });Latihan
Latihan memakai ketiga proyek. Semuanya login dengan Bearer token, jadi pola yang sama dipakai berulang. Semua endpoint bisa dicoba dulu di API Lab.
Profil admin lewat API
Login sebagai admin / admin123 lewat POST /api/login, lalu panggil GET /api/me dengan token itu.
Hasilstatus 200 dan role bernilai admin
Memuat tests/latihan/test_m13_latihan.py…
Memuat tests/latihan/m13-latihan.spec.ts…
Alur pesanan lengkap lewat API
Login sebagai customer, tambahkan produk id 2 dan id 7 ke keranjang, lalu buat pesanan lewat POST /api/orders. Cek status, id, status pesanan, dan totalnya.
Hasil201, id berawalan TQA-, status Baru, total 305000
Memuat tests/latihan/test_m13_latihan.py…
Memuat tests/latihan/m13-latihan.spec.ts…
Checkout lewat UI, cek lewat API
Login lewat UI, tambah Mouse Wireless, checkout sampai berhasil. Ambil token dari localStorage (tokoqa_token), lalu panggil GET /api/cart.
Hasilkeranjang di API kosong (items: [])
Memuat tests/latihan/test_m13_latihan.py…
Memuat tests/latihan/m13-latihan.spec.ts…
Mock daftar produk kosong
Intercept **/api/products dengan balasan {"total": 0, "data": []}, buka Arena, tekan Ambil produk (jurus 14), lalu cek jumlah produknya.
Hasil0 produk, dan daftarnya kosong
Memuat tests/latihan/test_m13_latihan.py…
Memuat tests/latihan/m13-latihan.spec.ts…
KlinikQA: token dan jam bentrok
Brief: KlinikQA memberi token saat login, sama seperti TokoQA.
- Panggil
GET /api/klinik/metanpa token. Pastikan 401. - Login sebagai
pasien/sehat123, ambiltokendari respons, lalu panggil/api/klinik/medengan headerAuthorization: Bearer <token>. - Buat janji temu, lalu kirim janji yang sama persis sekali lagi.
Hasil401, lalu role: "pasien", lalu 201, lalu 409 dengan pesan "sudah terisi"
Memuat tests/latihan/test_m13_latihan.py…
Memuat tests/latihan/m13-latihan.spec.ts…
PerpusQA: dua peran dengan login
Brief: di PerpusQA, login lewat API memberi token untuk header Authorization. Pustakawan mengelola, anggota meminjam.
- Pastikan
GET /api/perpus/metanpa token ditolak 401. - Buka perpustakaan lewat
POST /api/perpus/librariesdengan username acak, login sebagai pustakawan, lalu daftarkan anggota lewatPOST /api/perpus/membersdan login sebagai anggota itu. - Dengan token anggota, pinjam buku
bookId4. Dengan token pustakawan, pastikan pinjaman itu muncul diGET /api/perpus/loans?status=aktif. - Dengan token anggota, coba tambah buku lewat
POST /api/perpus/books. Pastikan ditolak. - Dengan token pustakawan, terima pengembalian bukunya, lalu pastikan pinjaman aktif anggota kembali 0.
- Logout sebagai anggota, lalu pastikan token-nya tidak berlaku lagi.
Hasil401, 201, pinjaman terlihat oleh pustakawan, 403 dengan reason: "role", activeLoans 0, lalu 401 dengan reason: "invalid"
Memuat tests/latihan/test_m13_latihan.py…
Memuat tests/latihan/m13-latihan.spec.ts…
Baca kontrak, lalu buktikan
Brief: baca kontrak sebelum menulis test. Buka API Spec untuk PATCH /api/perpus/books/{id}.
- Dari kontrak, cari tahu: peran mana yang boleh memanggilnya, dan kapan keluar status 422.
- Login dengan akun demo pustakawan, kirim
PATCHdengan body kosong, cek respons dengan helper kontrak, lalu pastikanfieldsberisi kunci_. - Login dengan akun demo anggota, kirim
PATCHberisi{"copies": 5}. Cek dengan helper kontrak dan pastikanreason-nyarole. - Ambil detail buku itu (
GET) dan cek juga dengan helper kontrak. Tidak ada data yang diubah, jadi aman memakai akun demo.
Hasil422, 403, lalu 200, dan ketiganya lolos pengecekan kontrak
Memuat tests/latihan/test_m13_latihan.py…
Memuat tests/latihan/m13-latihan.spec.ts…
Perhatikan bahwa kontrak sudah menjawab tugas 1 sebelum kamu menulis satu baris kode: hanya Pustakawan yang boleh, dan 422 keluar untuk field salah, field tidak dikenal, atau body kosong.
Dua peran sekaligus: admin dan customer
Brief: di TokoQA, admin memproses pesanan customer. Uji alur dua orang ini dalam satu test dengan dua browser context.
- Siapkan pesanan customer lewat API.
- Di context kedua, login sebagai admin lewat UI dan ubah status pesanan itu menjadi Dikirim.
- Di context customer, buka Pesanan saya.
Hasilcustomer melihat status Dikirim di pesanannya
Memuat tests/latihan/test_m13_latihan.py…
Memuat tests/latihan/m13-latihan.spec.ts…
Dua context punya cookie dan localStorage terpisah, jadi login admin tidak mengganggu sesi customer.