Tes E2E Vertikal: Toast Hijau Bukan Bukti Data Masuk
Satu test Playwright menembus tiga lapis: aksi UI, baris di database via docker exec, lalu search API, dengan cleanup permanen di dalam test.
Ringkasan
Toast hijau nggak njamin data masuk database, coba dong. Sekarang test-nya vertikal: isi form, cek langsung baris di MySQL via docker exec, terus query search API, semuanya sekali jalan. Cleanup-nya juga delete permanen di dalam test, jadi bug ketahuan di CI, bukan pas staging.
Tes E2E Vertikal Tiga Lapis: Toast Hijau Tidak Membuktikan Data Masuk
Test run pertama, semua lampu hijau. Form admin terisi, tombol Simpan diklik, toast "Item tersimpan" muncul di pojok kanan atas, dan Playwright melaporkan 1 test passed. Lalu saya buka database-nya sendiri, dan barisnya tidak ada.
Tidak ada error. Tidak ada log aneh. Toast tetap muncul.
Tebakan saya yang salah
Waktu itu saya sudah merasa cukup: kalau UI test hijau, berarti form-nya bekerja, dan kalau form-nya bekerja, datanya pasti masuk. Pemikiran itu salah. Form validation, API handler, dan DB write adalah tiga titik gagal yang berbeda, dan test saya hanya menguji yang pertama.
Yang sebenarnya terjadi waktu itu: handler API mengembalikan status sukses sebelum transaction commit selesai, sementara pesan toast dibuat dari response HTTP saja. UI tidak pernah tahu apakah baris benar-benar tertulis. Test saya percaya pada toast, dan toast mempercayai response yang terlalu optimistis.
Sejak itu saya menulis test secara vertikal: satu user flow diuji menembus tiga lapis sekaligus — aksi UI, baris di database, lalu query API — dan dibersihkan permanen di dalam test itu juga.
Tiga lapis dalam satu test
Pola yang saya pakai sekarang kira-kira begini. Playwright membuka form admin, mengisi satu item, lalu tidak berhenti di toast:
// tests/admin-create-item.spec.ts
import { test, expect } from '@playwright/test';
test('item baru muncul di DB dan di search API', async ({ page, request }) => {
// Lapis 1: aksi UI
await page.goto('/admin/items/new');
await page.getByLabel('Nama item').fill('Item Uji Vertikal');
await page.getByRole('button', { name: 'Simpan' }).click();
await expect(page.getByText('Item tersimpan')).toBeVisible();
// Lapis 2: baris di database, dicek langsung dari container
const cmd = `mysql -u$DB_USER -p$DB_PASS $DB_NAME -N -e ` +
`"SELECT COUNT(*) FROM items WHERE name = 'Item Uji Vertikal'"`;
const out = await execInDbContainer(cmd);
expect(Number(out.trim())).toBe(1);
// Lapis 3: search API dengan q=
const res = await request.get('/api/admin/items?q=Item%20Uji%20Vertikal');
expect(res.ok()).toBeTruthy();
const body = await res.json();
expect(body.items).toHaveLength(1);
// Cleanup permanen lewat endpoint delete yang sama
await request.delete(`/api/admin/items/${body.items[0].id}`);
});Helper `execInDbContainer` pada dasarnya hanya membungkus `docker exec`, perintah resmi Docker untuk menjalankan satu perintah baru di dalam container yang sedang berjalan [6], ke container database. Credential diambil dari environment variable, bukan ditulis di test. Kira-kira perintah yang dieksekusi:
docker exec app-db mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" \
-N -e "SELECT COUNT(*) FROM items WHERE name = 'Item Uji Vertikal'"Output yang diharapkan persis `1`. Kalau yang keluar `0`, itu artinya baris tidak pernah sampai ke database meski toast sudah muncul, dan test gagal di titik yang benar. Kalau perintahnya error karena container tidak jalan, pesannya jelas beda, dan itu masalah lingkungan, bukan masalah aplikasi.
Tiga lapis itu punya alasan masing-masing. UI memastikan user bisa menyelesaikan flow. Assert ke database memastikan data benar-benar persist. Query `q=` ke admin search API memastikan item yang baru dibuat bisa ditemukan lagi oleh sisi server, bukan hanya tersimpan diam-diam di tabel.
Bagian terakhir yang tidak boleh dilewat: delete-nya permanen dan terjadi di dalam test, bukan lewat script ops yang dijalankan minggu depan. Setelah `DELETE`, barisnya hilang. Tidak ada data sisa yang membuat run berikutnya bergantung pada run sebelumnya.
Versi panjang dari pola ini: buat data lewat seed API, uji flow-nya, hapus lewat API juga. Versi ini lebih cepat, tapi kehilangan inti masalahnya, yaitu bahwa jalur UI-nya sendiri harus terbukti menulis data. Saya memilih versi yang lewat form karena form-nya yang ingin saya percayai.
Yang berubah setelah memakai pola ini: bug yang dulunya baru ketahuan di staging, sekarang gagal di CI dalam hitungan detik, lengkap dengan lapis mana yang tidak cocok. Test jadi sedikit lebih lambat karena satu test melakukan lebih banyak hal. Buat saya itu trade-off yang jelas menguntungkan, karena satu test vertikal yang jujur lebih berguna daripada lima test yang hanya mempercayai toast.
Sources
1. API testing - Playwright 2. Best practices - Playwright 6. docker exec - Docker Docs