Test yang Membaca Source Code-nya Sendiri
Vitest nggak bisa merender komponen di Vite 8? Jadikan file source sebagai data test: kunci invarian flag rating dengan toContain.
Ringkasan
Test kartu di KotaPortal error karena Vite 8 udah pake rolldown jadi komponen React nggak bisa dirender di vitest. Akhirnya bikin tripwire yang baca file source sebagai teks buat mastiin rating_enabled !== false tetap ada di enam permukaan kartu. Cara ini jaga badge rating nggak bocor, sambil nunggu plugin React beneran kepasang buat test render.
Test render buat komponen kartu entitas di KotaPortal baru selesai saya tulis, dan begitu vitest dijalankan, yang keluar bukan hasil tes tapi dinding error transformasi JSX. Pipeline test menolak memproses sintaks React sama sekali. Penyebabnya bukan di kode saya: environment test ini jalan di atas Vite 8 yang toolchain-nya sudah pindah ke rolldown dan oxc [3], sementara plugin React untuk pipeline vitest belum terpasang. Akibatnya semua komponen UI nggak bisa dirender di test, termasuk kartu yang mau saya uji.
Dugaan pertama saya waktu itu: biarin aja, gate rating ini nggak usah diuji dulu, toh logikanya udah ada di backend. Pikiran itu saya bantah sendiri. Pengaman yang nggak pernah diuji itu ilusi; ada saja suatu hari kondisi pengecekannya kehapus, dan badge rating muncul di permukaan yang seharusnya dibungkam. Saya butuh sesuatu yang berteriak merah di CI begitu itu terjadi.
Membaca Source Code Jadi Bahan Test
Solusinya malah lebih sederhana dari dugaan saya: karena komponennya nggak bisa dieksekusi, saya jadikan file source-nya sebagai datanya. Test-nya membaca file asli sebagai teks, lalu menguji invarian yang wajib ada di dalamnya.
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const read = (rel: string) =>
readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
const SURFACES = [
"entity-card", "entity-list-item", "hero-section",
"map-point-card", "detail-panel", "map-popup-content",
];
describe("Rating gate", () => {
it("gates rating on every card surface", () => {
for (const name of SURFACES) {
const src = read(`../features/${name}.tsx`);
expect(src).toContain("rating_enabled !== false");
}
});
});
Ada satu kebiasaan yang ikut saya bawa dari file test aslinya: daftar permukaannya nggak ditulis manual. Daftar `SURFACES` di test itu satu-satunya tempat yang harus di-update kalau nanti ada permukaan kartu baru. Nggak ada duplikasi daftar di dua tempat, nggak ada daftar yang bisa basi. Regenerasi testing-nya juga kebayang: mau nambah permukaan? Tambah satu string, selesai.
Dua primitifnya sepele tapi terdokumentasi resmi: `toContain` bisa dipakai buat mengecek substring dari sebuah string [1], dan `toMatch` menerima pola regular expression [1]. Sementara `readFileSync` bisa menerima objek URL WHATWG sebagai path-nya [2], makanya pola `new URL(rel, import.meta.url)` aman dipakai relatif terhadap lokasi file test.
Tripwire, Batasnya, dan Pasangan Backend-nya
Cara ini fungsinya kayak tripwire. Begitu ada yang menghapus `rating_enabled !== false` dari satu saja dari enam permukaan (kartu entitas, item list, hero home, kartu titik peta, popup, sampai panel detail), CI langsung merah. Saya juga mengunci jalur datanya: satu regex via `toMatch` memastikan conditional spread rating ke item favorit tetap ada.
Tanpa spread itu, `avg_rating` dan `review_count` bisa diam-diam bocor ke item favorit padahal badge-nya udah dimatikan.
Kuncinya ada di ketelitian kecil. Nama variabelnya beda-beda per berkas: `entity.` di kartu, `slide.` di hero, `point.` di panel peta. Regex-nya saya tulis longgar soal prefiks tapi ketat soal idiom spread-nya, biar refactor nama variabel nggak bikin alarm palsu. Pola seperti ini juga nyambung sama cerita implementasi palsu di implementasi-palsu-wajib-gagal: pengaman yang nggak pernah bisa gagal itu cuma hiasan.
Batasnya juga harus saya akui. Membaca teks cuma membuktikan bentuk kode, bukan perilaku saat aplikasi jalan. Karena itu di backend ada pasangannya: integration test yang membuktikan flag `rating_enabled` benar-benar ikut ke payload kartu, sementara agregat rating yang tersimpan nggak disentuh sama sekali. Flag ini memgate tampilan, bukan data. Detail yang bikin saya sempat mikir: tipenya nggak seragam, payload kartu membawa bool biasa, titik peta membawa pointer ke bool biar bisa membedakan belum-diisi dan memang-nonaktif. Probe pertama saya nyasar nyari pointer di payload kartu; ternyata pointer-nya cuma di sisi peta.
Saya pribadi lebih suka merawat pasangan ini daripada berhari-hari berkelahi dengan konfigurasi build tool cuma buat merender satu komponen; pipeline yang stabil lebih berharga daripada coverage yang cakep di laporan tapi gampang retak. Test render sendiri nggak saya buang permanen. Nanti kalau plugin React udah masuk pipeline test, render test tetap jadi target. Selama belum, invarian teks yang jaga.
Peralihan Vite 8 ke rolldown dan oxc [3] mungkin cuma jadi catatan kaki di panduan migrasinya, tapi buat saya itu pengingat: pas tooling test nggak bisa menjalankan kode, masih ada satu ruang terakhir buat menguji, yaitu teks kode itu sendiri.
Sources
3. mungkin
1. vitest expect API
2. Node.js fs docs
3. panduan migrasi Vite 8