Skip to content

Route Smoke Test yang Berhenti di Status 200

Adityo Guni Waluyo

Crawl test hijau semua karena tiap route balik 200, padahal halaman yang dibuka kosong. Perbaikannya: satu elemen kunci per route.

Ringkasan

Test route yang cuma cek status 200 bisa lolos padahal halamannya cuma shell kosong, karena 200 nggak menjamin React render apa-apa. Solusinya pakai key-element map: tiap route punya satu elemen kunci yang dicek pakai expect(locator).toBeAttached(), bukan toBeVisible() biar aman buat layout responsive. Jangan lupa key element harus muncul di semua render state dan timeout dinaikkan buat dev server yang lambat.

Route Smoke Test yang Berhenti di Status 200

Malam itu crawl test L1 di repo admin saya lulus semuanya, semua route hijau. Lalu saya buka tiga halaman secara manual, dan yang muncul hanya shell kosong: header ada, sidebar ada, isi halaman tidak ada. Test-nya tetap bilang lolos karena tiap route memang balik 200.

Salah duga saya persis di situ. Selama ini saya pegang aturan praktis yang keliru: kalau route balik 200, halaman pasti hidup. Padahal 200 hanya membuktikan server mengirim sesuatu, bukan bahwa React sempat render apa pun. Empty shell adalah 200 yang sukses.

Perbaikannya bukan menambah jumlah test, tapi mengganti pertanyaannya. Tiap route tidak lagi ditanya "apakah merespons", melainkan "apakah elemen intinya muncul". Saya sebut ini key-element map, dan formatnya sederhana: tiap route statis admin dapat tepat satu elemen yang membuktikan halaman sudah render isinya. Bisa testid tabel, bisa label form, bisa heading; semua di-search lewat locator, komponen utama mekanisme auto-waiting Playwright [5]. Satu elemen, satu route, tanpa pengecualian.

Satu elemen kunci per route

Bentuk peta-nya sebuah object: key-nya path route, value-nya satu locator Playwright.

// tests/l1.routes.spec.ts
import { test, expect, type Locator } from '@playwright/test';

const keyElements: Record<string, Locator> = {
  '/admin/orders': page.getByTestId('orders-table'),
  '/admin/settings': page.getByLabel('Nama toko'),
  '/admin/reports': page.getByRole('heading', { name: 'Laporan' }),
  '/admin/promos': page.getByLabel('Buat promo')
    .or(page.getByTestId('promo-empty')),
};

for (const [route, key] of Object.entries(keyElements)) {
  test(`L1 render ${route}`, async ({ page }) => {
    test.setTimeout(45_000);
    await page.goto(route);
    await expect(key.first()).toBeAttached();
  });
}

Tiga baris terakhir itu inti test-nya. Semua keputusan yang bikin test ini awet ada di sana, dan sayangnya saya menemukannya lewat dua kegagalan berturut-turut pada run pertama.

Dua kegagalan pertama yang justru berguna

Kegagalan pertama memalukan: saya menulis await locator.toBeAttached() langsung di locator-nya. Itu bukan API, jadi run langsung error sebelum sempat mengecek apa pun. Playwright bekerja dengan model web-first assertion, assertion auto-retrying-nya menunggu kondisi terpenuhi dan harus lewat expect(...), bukan dipanggil sebagai method locator [9]. Bentuk benarnya await expect(locator).toBeAttached(). Bedanya bukan gaya penulisan: lewat expect, assertion me-re-fetch elemen berulang sampai kondisinya terpenuhi atau timeout habis [9]; memanggil method yang tidak ada hanya memberi TypeError.

Kegagalan kedua lebih menarik. Route dropdown sempat saya beri key element berupa testid salah satu option-nya. Test-nya gagal terus, padahal option itu jelas ada saat saya cek manual. Ternyata testid option hanya ikut terender ketika dropdown dalam keadaan terbuka. Dalam keadaan tertutup, DOM-nya memang tidak memuatnya.

Di situ saya membuat aturan yang sekarang tidak saya langgar: key element harus ada di setiap render state. Empty state tetap dihitung render state yang valid. Dropdown tertutup tidak.

Jadi untuk route itu, key element-nya saya ganti ke aria-label pada trigger yang tertutup, karena atribut itu ada dari detik pertama render. Untuk route yang punya kondisi kosong dan terisi, saya pakai locator.or() yang menggabungkan dua locator jadi satu [8], seperti pada route promo di contoh: label "Buat promo" untuk state kosong, testid untuk state berisi. Kalau salah satu muncul, route-nya lolos.

Dua detail yang bikin test tidak rapuh

Hampir semua halaman admin di project ini responsive, dan pola render-nya dua DOM sekaligus: satu untuk desktop, satu untuk mobile, salah satunya selalu hidden. Kalau assertion-nya toBeVisible(), test akan gagal untuk versi yang memang tidak ditampilkan di viewport test. Karena yang saya butuh cuma bukti halaman sempat render, toBeAttached() sudah cukup, asersi itu hanya menuntut elemen menempel di DOM tanpa mewajibkan terlihat [9], dan .first() dipakai untuk menangani DOM yang kembar.

Soal timing: dev server di mesin CI masih meng-compile route baru di request pertama, dan angkanya bisa lewat 15 detik per route yang belum pernah dikunjungi. Default timeout per assertion terlalu mepet untuk itu. Karena tiap file test hanya menjalankan satu route, saya naikkan ke 45 detik lewat test.setTimeout() di dalam test, bukan global, supaya route yang memang cepat tidak ikut diberi toleransi longgar.

Cek cepatnya: jalankan test dengan satu route yang key element-nya sengaja dihapus dari komponen. Test harus gagal, dan pesannya harus menyebut locator itu. Kalau test tetap lolos, elemen itu tidak benar-benar jadi kunci.

Ada yang bilang smoke test level satu terlalu dangkal untuk diperbaiki dan lebih baik langsung naik ke L2. Saya tidak setuju. L1 adalah lapisan paling murah yang masih membuktikan rendering, dan cost-nya per route cuma satu kali goto plus satu assertion. Membiarkan L1 lolos atas shell kosong justru membuat seluruh lapisan di atasnya membuang waktu pada sesuatu yang rusak sejak awal.

Bonusnya tidak kecil: key-element map adalah dokumentasi yang tidak bisa basi soal apa fungsi tiap halaman. Kalau seorang kontributor baru bertanya apa yang membuat halaman promo dianggap "hidup", jawabannya ada di spec, bukan di ingatan orang lama. Kalau suatu hari halaman itu berubah fungsi, test gagal dan memaksa peta-nya ikut diperbarui. Itu yang saya mau dari sebuah test murah.

Sources

5. Locators - Playwright 8. Locator - Playwright 9. Assertions - Playwright

Artikel terkait