Skip to content

Ketika Tabel Migrasi Berbohong di Test Integrasi Bidang

Adityo Guni Waluyo

Suite hijau padahal skema test DB basi: schema_migrations cuma catat file migrasi, volume Docker lama diam-diam bawa skema lama.

Ringkasan

Test ijo semua tapi gue curiga kolom bidang belum kebawa di DB beneran. Ternyata volume MySQL lama masih kepake, migrate ngeliat versi 58 jadi skip padahal skema basi. Sekarang tiap test dibersihin total pake down --volumes biar migrasi jalan dari nol dan hasilnya beneran valid.

Semua test hijau, tapi saya nggak bisa berhenti curiga. Baru aja menambahkan kolom bidang ke tabel announcements, dan pas menjalankan suite integrasi lawan MySQL 5.7 beneran, satu pertanyaan nyasar: benarkah database test-nya udah bawa kolom itu?

Saya buka tabel schema_migrations. Tertulis rapi: versi 58. Tebakan pertama saya, kalau versinya udah paling baru, berarti skemanya pasti ikut baru. Ditambah lagi, nggak ada test yang merah. Ya sudah, lanjut.

Ternyata dua-duanya salah. Tabel versi cuma mencatat file migrasi mana yang pernah dieksekusi, bukan isi sebenarnya dari volume tempat database itu hidup. Dua sinyal yang sama-sama terasa kuat itu bisa bohong serentak.

Volume sisa sesi kemarin

Akar masalahnya ada di lingkungan, bukan di kode. File docker-compose.test.yml buat MySQL 5.7 itu disposable, tapi volume-nya named dan nempel antar-jalan. Skema lama dari sebelum kolom bidang ada masih utuh di situ. golang-migrate yang jalannya berdasar urutan versi sesuai dokumen resminya [3] melihat schema_migrations sudah di 58, tidak ada file baru yang perlu dijalanin, lalu pulang dengan tenang.

Jadi angka versi itu meyakinkan sekali, padahal cuma berkata soal file yang pernah dieksekusi. Skema di volume bisa basi tanpa ada yang sadar. False positive kayak ini paling bahaya: nggak ada alarm, hasilnya malah hijau semua.

Beresin dari sisi higiene lingkungan

Solusinya bukan di kode Go. Teardown diganti jadi docker compose down --volumes, yang menurut dokumentasi resmi Compose menghapus named volume yang dideklarasikan di file Compose beserta anonymous volume yang nempel ke kontainer di halaman perintahnya [2]. Volume dijamin kebentuk lagi dari nol, testutil.SetupTestDB jalanin migrasi dari awal, dan kolom bidang benar-benar ada sebelum test pertama dieksekusi.

Matriks scope yang akhirnya terkunci

Setelah lingkungan bersih, baru deh nilai sesungguhnya dari file test ini kerasa. Enam skenario scope bidang sekarang dicek sama database sungguhan, bukan mock: listing pakai scope olahraga cuma melihat konten global plus miliknya, scope ALL melihat ketiganya, update lintas bidang kena ErrForbidden yang di API jadi 403, user ber-bidang ditolak waktu mau menyunting konten global, dan DeletePermanent oleh bidang lain ditolak juga. Testnya dibungkus build tag integration, jadi cuma ikut pas dipanggil eksplisit dan nggak ngeramein unit test harian.

//go:build integration

func TestAnnouncementBidangScopeCRUD(t *testing.T) {
    db := testutil.SetupTestDB(t)
    repo := announcementadmin.NewMySQLRepository(db)
    svc := announcementadmin.NewService(repo, nil)

    // listing scope olahraga: global (NULL) + olahraga saja
    items, total, err := repo.List(ctx, announcementadmin.ListQuery{
        Status: "published", Bidang: scope.Olahraga,
    })
    if total != 2 {
        t.Fatalf("total = %d, want 2", total)
    }
}

Sekalian, dua call site ListCategories yang masih pakai signature lama ikut diperbarui ke bentuk yang terfilter modul. Test yang dulu basi diam-diam itu ke-update sekalian. Dari siklus ini saya ambil satu keputusan tetap: tabel versi nggak lagi dianggap satu-satunya sumber kebenaran soal skema. Test environment harus dihancurkan total dulu sebelum dipakai, biar yang hijau beneran hijau.

Mekanismenya luput justru karena nggak ada yang kelihatan rusak. Kalau ada file migrasi baru yang menyuruh ALTER tabel, MySQL bakal protes keras begitu skemanya bentrok. Yang berbahaya justru skenario tenang kayak gini: volume lama, nggak ada file baru buat dijalanin, suite unit tetap hijau, dan satu-satunya jejak bahwa ada yang nggak pas cuma suara di kepala saya sendiri. Kalau test integration-nya keburu dipercaya, hasilnya pun nggak bisa dipakai argumentasi: test yang katanya nutup kolom bidang itu sebenernya nggak pernah menyentuh kolomnya, karena kolomnya memang nggak ada di database yang dipakai.

Harga dari disiplin ini jelas: tiap ronde integrasi bayar boot MySQL dari nol plus migrasi penuh dari awal. Makanya test semacam ini sengaja dibungkus build tag integration dan didaftarkan di indeks scripts/unit_testing biar cuma jalan pas memang dipanggil, bukan tiap commit. Matriks enam skenario itu emang nggak perlu jalan tiap menit; dia jalan tiap ada perubahan di jalur scope bidang. Dan pas itu terjadi, kepastian soal skema bukan bonus, melainkan syarat biar hasil test bisa dipercaya.

Sources

Artikel terkait