Setiap migrasi skema, setiap “perbaikan kecil” pada query, dan setiap pemutakhiran versi PostgreSQL membawa pertanyaan yang sama: apakah perilaku lama yang sudah benar tetap benar? Bug regresi muncul persis di sana — fitur baru jalan mulus, tetapi laporan bulanan yang sudah setahun aman tiba-tiba salah angka. Jawabannya bukan menguji lebih teliti sekali saja, melainkan mengemas pengujian agar dijalankan ulang secara otomatis setiap kali ada perubahan. Chapter ini merakit regression suite dari bahan-bahan chapter-chapter sebelumnya: pustaka asersi, file uji bernomor, skrip runner dengan kode keluar, dan golden file — lalu menutup dengan pengenalan pgTAP dan pola integrasi CI.
Kapan Regression Test Dijalankan #
Regression suite adalah asuransi; premiannya dibayar setiap perubahan, bukan setiap bulan. Momen-momen yang wajib menjalankannya:
- Sebelum dan sesudah migrasi skema —
ALTER TABLEyang “tak berbahaya” bisa mengubah urutan kolomSELECT *, memicu trigger lain, atau mengubah tipe keluaran function. - Setiap perubahan pada query penting — laporan, Dashboard, query ETL: bandingkan hasil sebelum dan sesudah refactor lewat asersi.
- Sebelum upgrade PostgreSQL — perilaku kecil bisa bergeser antar versi major; suite yang hijau sebelum upgrade dan sesudahnya adalah bukti aman.
- Setiap perubahan pada function dan trigger — objek yang diuji di chapter sebelumnya kini dijalankan ulang massal.
Pustaka Asersi #
Langkah pertama adalah mengubah pola CASE WHEN yang berulang-ulang menjadi function yang bisa dipakai ulang. File ini adalah “framework” satu-file suite kita — dua asersi: assert_equal untuk nilai tunggal dan assert_set_equal untuk himpunan hasil query:
-- Pustaka asersi mini untuk regression suite.
-- CREATE OR REPLACE membuat file ini aman dijalankan berulang kali.
CREATE OR REPLACE FUNCTION assert_equal(
aktual anyelement,
ekspektasi anyelement,
label text
) RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
IF aktual IS NOT DISTINCT FROM ekspektasi THEN
RETURN 'LULUS - ' || label;
ELSE
RETURN 'GAGAL - ' || label
|| ': aktual=' || COALESCE(aktual::text, 'NULL')
|| ' ekspektasi=' || COALESCE(ekspektasi::text, 'NULL');
END IF;
END $$;
-- Asersi himpunan: dua query harus menghasilkan baris yang identik.
-- Perbandingan memakai EXCEPT dua arah (pola chapter Menguji Query SQL).
CREATE OR REPLACE FUNCTION assert_set_equal(
query_aktual text,
query_ekspektasi text,
label text
) RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_selisih integer;
BEGIN
EXECUTE '
WITH aktual AS (' || query_aktual || '),
ekspektasi AS (' || query_ekspektasi || ')
SELECT count(*) FROM (
(SELECT * FROM aktual EXCEPT SELECT * FROM ekspektasi)
UNION ALL
(SELECT * FROM ekspektasi EXCEPT SELECT * FROM aktual)
) selisih'
INTO v_selisih;
IF v_selisih = 0 THEN
RETURN 'LULUS - ' || label;
ELSE
RETURN 'GAGAL - ' || label || ': ' || v_selisih || ' baris selisih';
END IF;
END $$;Output yang Dihasilkan:
CREATE FUNCTION
CREATE FUNCTIONBedah desainnya:
anyelementmenerima tipe apa saja — satu function untuk integer, text, numeric, tanggal; tipe aktual dan ekspektasi harus cocok (panggil dengan cast eksplisit seperti5::bigintbila perlu).IS NOT DISTINCT FROMsebagai pembanding — kasus NULL tercakup otomatis; NULL dibandingkan NULL dianggap lulus.- Pesan gagal memuat kedua nilai —
aktual=... ekspektasi=...— sehingga log CI langsung menunjukkan seberapa jauh meleset. assert_set_equalmembungkus pola EXCEPT dua arah dari chapter Menguji Query SQL dalamEXECUTE— dua query diterima sebagai string teks, selisihnya dihitung di server.
File Uji dengan Konvensi Nomor #
Suite yang tumbuh butuh konvensi agar runner bisa menjalankannya berurutan. Konvensi di sini: prefix nomor (001-, 002-, …) untuk urutan, isi file idempoten (DROP ... IF EXISTS) agar suite aman diulang, dan setiap file membangun fixture-nya sendiri:
-- 001-uji-stok.sql — satu file regression test untuk aturan stok minimum.
-- Konvensi: nama file diawali nomor urut; DROP ... IF EXISTS agar bisa
-- dijalankan ulang berulang kali (idempoten).
DROP TABLE IF EXISTS barang CASCADE;
CREATE TABLE barang (
kode text PRIMARY KEY,
nama text NOT NULL,
stok integer NOT NULL,
stok_minimum integer NOT NULL
);
INSERT INTO barang VALUES
('BRG-001', 'Beras 5kg', 40, 10),
('BRG-002', 'Minyak Goreng 1L', 8, 12),
('BRG-003', 'Gula Pasir 1kg', 6, 12),
('BRG-004', 'Teh Kotak', 30, 10),
('BRG-005', 'Kopi Bubuk 200g', 15, 8);
SELECT assert_equal(
(SELECT count(*) FROM barang),
5::bigint,
'fixture berisi 5 barang'
);
SELECT assert_equal(
(SELECT count(*) FROM barang WHERE stok < stok_minimum),
2::bigint,
'dua barang di bawah stok minimum (BRG-002, BRG-003)'
);
SELECT assert_set_equal(
'SELECT kode FROM barang WHERE stok < stok_minimum ORDER BY kode',
$$SELECT unnest(ARRAY['BRG-002', 'BRG-003'])$$,
'kode barang yang perlu restock tepat BRG-002 dan BRG-003'
);
SELECT assert_equal(
(SELECT min(stok) FROM barang),
6::integer,
'stok terendah di fixture adalah 6'
);Output yang Dihasilkan:
DROP TABLE
CREATE TABLE
INSERT 0 5
assert_equal
---------------------------------
LULUS - fixture berisi 5 barang
(1 row)
assert_equal
-------------------------------------------------------------
LULUS - dua barang di bawah stok minimum (BRG-002, BRG-003)
(1 row)
assert_set_equal
------------------------------------------------------------------
LULUS - kode barang yang perlu restock tepat BRG-002 dan BRG-003
(1 row)
assert_equal
-------------------------------------------
LULUS - stok terendah di fixture adalah 6
(1 row)Perhatikan pemanggilan assert_set_equal: query aktual dan ekspektasi dikirim sebagai string. Kasus unnest(ARRAY['BRG-002', 'BRG-003']) memperlihatkan kekuatan pola ini — ekspektasi yang rumit ditulis sebagai data (array literal), bukan query duplikat dari implementasi. Empat asersi ini kini menjadi spesifikasi aturan stok minimum yang hidup: bila seseorang mengubah definisi “perlu restock”, suite ini yang bersuara.
Skrip Runner #
Terakhir, otomatisasinya: skrip bash yang menjalankan seluruh file uji berurutan, menampilkan hasilnya, dan keluar dengan kode error bila ada kegagalan — kode inilah yang dibaca pipeline CI:
#!/usr/bin/env bash
# Runner regression suite: jalankan tiap file uji berurutan, gagal bila
# ada teks GAGAL di hasil asersi atau error SQL yang menghentikan file.
set -u
cd "$(dirname "$0")"
berkas_uji=(assert-lib.sql 001-uji-stok.sql)
ada_gagal=0
for f in "${berkas_uji[@]}"; do
echo "==> menjalankan $f"
keluaran=$(psql -X -q -v ON_ERROR_STOP=1 -f "$f" 2>&1)
if [ $? -ne 0 ]; then
echo "$keluaran"
echo "GAGAL: $f berhenti karena error SQL"
exit 1
fi
if [ -n "$keluaran" ]; then
echo "$keluaran"
fi
if printf '%s' "$keluaran" | grep -q 'GAGAL'; then
ada_gagal=1
fi
done
if [ "$ada_gagal" -eq 1 ]; then
echo "SUIT REGRESI: GAGAL"
exit 1
fi
echo "SUIT REGRESI: LULUS"Output yang Dihasilkan:
==> menjalankan assert-lib.sql
==> menjalankan 001-uji-stok.sql
assert_equal
---------------------------------
LULUS - fixture berisi 5 barang
(1 row)
assert_equal
-------------------------------------------------------------
LULUS - dua barang di bawah stok minimum (BRG-002, BRG-003)
(1 row)
assert_set_equal
------------------------------------------------------------------
LULUS - kode barang yang perlu restock tepat BRG-002 dan BRG-003
(1 row)
assert_equal
-------------------------------------------
LULUS - stok terendah di fixture adalah 6
(1 row)
SUIT REGRESI: LULUSMekanismenya tiga lapis:
ON_ERROR_STOP=1— error SQL apa pun menghentikan file dan membuatpsqlkeluar kode 3; runner menangkapnya lewat$?dan langsung gagal.grep -q 'GAGAL'— asersi yang lulus sql-nya tetapi gagal logikanya (barisGAGAL - ...) terdeteksi dari teksnya.exit 1pada akhir — suite yang gagal memberi kode keluar non-zero ke pemanggilnya, sehinggamake test, cron, atau CI tahu harus merah.
Jalankan dari dalam folder chapternya, suite ini selesai dalam hitungan detik — dan karena idempoten, menjalankannya dua kali memberi hasil yang persis sama.
Golden File: Mengunci Output Laporan #
Asersi membandingkan nilai; golden file mengunci tampilan utuh sebuah laporan. Polanya: simpan output yang sudah terverifikasi ke file ekspektasi, lalu setiap kali suite berjalan, hasil segar dibandingkan dengannya lewat diff. Laporan yang dilindungi:
-- Laporan yang dilindungi golden file: format dan isinya dikunci
-- lewat hasil-ekspektasi.txt; perubahan apa pun di output akan terdeteksi.
SELECT kode,
nama,
stok,
stok_minimum,
CASE WHEN stok < stok_minimum THEN 'RESTOCK' ELSE 'OK' END AS status_stok
FROM barang
ORDER BY kode;Output yang Dihasilkan:
kode | nama | stok | stok_minimum | status_stok
---------+------------------+------+--------------+-------------
BRG-001 | Beras 5kg | 40 | 10 | OK
BRG-002 | Minyak Goreng 1L | 8 | 12 | RESTOCK
BRG-003 | Gula Pasir 1kg | 6 | 12 | RESTOCK
BRG-004 | Teh Kotak | 30 | 10 | OK
BRG-005 | Kopi Bubuk 200g | 15 | 8 | OK
(5 rows)File hasil-ekspektasi.txt di bundle chapter menyimpan output di atas — dibuat sekali dari eksekusi nyata, lalu menjadi patokan:
kode | nama | stok | stok_minimum | status_stok
---------+------------------+------+--------------+-------------
BRG-001 | Beras 5kg | 40 | 10 | OK
BRG-002 | Minyak Goreng 1L | 8 | 12 | RESTOCK
BRG-003 | Gula Pasir 1kg | 6 | 12 | RESTOCK
BRG-004 | Teh Kotak | 30 | 10 | OK
BRG-005 | Kopi Bubuk 200g | 15 | 8 | OK
(5 rows)Perbandingannya dilakukan skrip kecil ini:
#!/usr/bin/env bash
# Golden test: jalankan laporan, bandingkan output-nya dengan hasil-ekspektasi.txt.
# diff tanpa keluaran berarti identik; selisih apa pun menjadi kegagalan suite.
set -u
cd "$(dirname "$0")"
tmp=$(mktemp)
psql -X -q -f laporan-stok.sql > "$tmp"
if diff -u hasil-ekspektasi.txt "$tmp"; then
echo "GOLDEN: LULUS (output laporan identik dengan ekspektasi)"
rc=0
else
echo "GOLDEN: GAGAL (output berubah — periksa selisih di atas)"
rc=1
fi
rm -f "$tmp"
exit $rcOutput yang Dihasilkan:
GOLDEN: LULUS (output laporan identik dengan ekspektasi)Kekuatan golden file ada pada sensitivitasnya: kolom bergeser satu spasi, label berubah dari RESTOCK menjadi restock, atau satu baris hilang — semuanya menjadi selisih diff yang terlihat mata. Kelemahannya juga itu: perubahan yang disengaja (menambah kolom baru) mengharuskan golden file diregenerasi. Gunakan untuk laporan yang formatnya kontrak — ekspor untuk sistem lain, tagihan, dokumen resmi.
pgTAP: Kerangka Pengujian Khusus PostgreSQL #
Semua yang kita rakit di atas murni SQL dan bash. Ekosistem PostgreSQL punya kerangka pengujian yang lebih kaya: pgTAP, kumpulan function assertion bergaya xUnit yang berjalan di dalam database. Contoh bentuknya:
-- Ilustrasi pgTAP — TIDAK dijalankan di lingkungan kursus (pgTAP tidak terpasang).
BEGIN;
SELECT plan(3); -- umumkan jumlah uji
SELECT has_table('barang', 'tabel barang ada'); -- uji skema
SELECT has_column('barang', 'stok', 'kolom stok ada');
SELECT is(
(SELECT count(*) FROM barang WHERE stok < 0),
0::bigint,
'tidak ada stok negatif'
);
SELECT * FROM finish(); -- tutup: cetak ringkasan 1..3
ROLLBACK;plan() mengumumkan jumlah uji di awal, has_table()/has_column() memeriksa skema, is() membandingkan nilai, dan finish() mencetak ringkasan bernomor gaya TAP (1..3) sekaligus memastikan jumlah uji yang berjalan sama dengan rencana. Menjalankannya butuh instalasi extension (CREATE EXTENSION pgtap;) yang menyediakan ratusan asersi siap pakai — dari fk_ok() sampai results_eq() yang membandingkan hasil dua query.
Integrasi ke CI #
Regression suite memberi nilai penuh saat dijalankan otomatis. Pola GitHub Actions yang paling sederhana — service container PostgreSQL, migrasi, lalu suite:
# Ilustrasi alur CI: satu job, tiga langkah.
jobs:
regresi:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: YOUR_PASSWORD
ports: ["5432:5432"]
steps:
- uses: actions/checkout@v4
- name: Terapkan migrasi
run: psql -f migrasi/001_skema.sql
- name: Jalankan regression suite
run: bash pengujian/jalankan-suite.shLangkah terakhir itulah titik kuncinya: runner kita sudah keluar dengan kode non-zero saat gagal, dan GitHub Actions menandai build merah otomatis — tanpa satu baris integrasi tambahan. Karena suite membangun fixture-nya sendiri dan idempoten, CI bisa menjalankannya dari database kosong setiap kali.
Suite yang hijau memberi rasa aman; suite yang merah memberi arah. Tetapi ada kelas masalah yang hidupnya di luar suite: server yang tidak mau tersambung, query yang menggantung, disk yang penuh. Chapter penutup section ini menyusun kotak P3K untuk situasi-situasi tersebut.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
001-uji-stok.sql
Unduh berkas-- 001-uji-stok.sql — satu file regression test untuk aturan stok minimum.
-- Konvensi: nama file diawali nomor urut; DROP ... IF EXISTS agar bisa
-- dijalankan ulang berulang kali (idempoten).
DROP TABLE IF EXISTS barang CASCADE;
CREATE TABLE barang (
kode text PRIMARY KEY,
nama text NOT NULL,
stok integer NOT NULL,
stok_minimum integer NOT NULL
);
INSERT INTO barang VALUES
('BRG-001', 'Beras 5kg', 40, 10),
('BRG-002', 'Minyak Goreng 1L', 8, 12),
('BRG-003', 'Gula Pasir 1kg', 6, 12),
('BRG-004', 'Teh Kotak', 30, 10),
('BRG-005', 'Kopi Bubuk 200g', 15, 8);
SELECT assert_equal(
(SELECT count(*) FROM barang),
5::bigint,
'fixture berisi 5 barang'
);
SELECT assert_equal(
(SELECT count(*) FROM barang WHERE stok < stok_minimum),
2::bigint,
'dua barang di bawah stok minimum (BRG-002, BRG-003)'
);
SELECT assert_set_equal(
'SELECT kode FROM barang WHERE stok < stok_minimum ORDER BY kode',
$$SELECT unnest(ARRAY['BRG-002', 'BRG-003'])$$,
'kode barang yang perlu restock tepat BRG-002 dan BRG-003'
);
SELECT assert_equal(
(SELECT min(stok) FROM barang),
6::integer,
'stok terendah di fixture adalah 6'
);
assert-lib.sql
Unduh berkas-- Pustaka asersi mini untuk regression suite.
-- CREATE OR REPLACE membuat file ini aman dijalankan berulang kali.
CREATE OR REPLACE FUNCTION assert_equal(
aktual anyelement,
ekspektasi anyelement,
label text
) RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
IF aktual IS NOT DISTINCT FROM ekspektasi THEN
RETURN 'LULUS - ' || label;
ELSE
RETURN 'GAGAL - ' || label
|| ': aktual=' || COALESCE(aktual::text, 'NULL')
|| ' ekspektasi=' || COALESCE(ekspektasi::text, 'NULL');
END IF;
END $$;
-- Asersi himpunan: dua query harus menghasilkan baris yang identik.
-- Perbandingan memakai EXCEPT dua arah (pola chapter Menguji Query SQL).
CREATE OR REPLACE FUNCTION assert_set_equal(
query_aktual text,
query_ekspektasi text,
label text
) RETURNS text
LANGUAGE plpgsql
AS $$
DECLARE
v_selisih integer;
BEGIN
EXECUTE '
WITH aktual AS (' || query_aktual || '),
ekspektasi AS (' || query_ekspektasi || ')
SELECT count(*) FROM (
(SELECT * FROM aktual EXCEPT SELECT * FROM ekspektasi)
UNION ALL
(SELECT * FROM ekspektasi EXCEPT SELECT * FROM aktual)
) selisih'
INTO v_selisih;
IF v_selisih = 0 THEN
RETURN 'LULUS - ' || label;
ELSE
RETURN 'GAGAL - ' || label || ': ' || v_selisih || ' baris selisih';
END IF;
END $$;
cek-golden.sh
Unduh berkas#!/usr/bin/env bash
# Golden test: jalankan laporan, bandingkan output-nya dengan hasil-ekspektasi.txt.
# diff tanpa keluaran berarti identik; selisih apa pun menjadi kegagalan suite.
set -u
cd "$(dirname "$0")"
tmp=$(mktemp)
psql -X -q -f laporan-stok.sql > "$tmp"
if diff -u hasil-ekspektasi.txt "$tmp"; then
echo "GOLDEN: LULUS (output laporan identik dengan ekspektasi)"
rc=0
else
echo "GOLDEN: GAGAL (output berubah — periksa selisih di atas)"
rc=1
fi
rm -f "$tmp"
exit $rc
jalankan-suite.sh
Unduh berkas#!/usr/bin/env bash
# Runner regression suite: jalankan tiap file uji berurutan, gagal bila
# ada teks GAGAL di hasil asersi atau error SQL yang menghentikan file.
set -u
cd "$(dirname "$0")"
berkas_uji=(assert-lib.sql 001-uji-stok.sql)
ada_gagal=0
for f in "${berkas_uji[@]}"; do
echo "==> menjalankan $f"
keluaran=$(psql -X -q -v ON_ERROR_STOP=1 -f "$f" 2>&1)
if [ $? -ne 0 ]; then
echo "$keluaran"
echo "GAGAL: $f berhenti karena error SQL"
exit 1
fi
if [ -n "$keluaran" ]; then
echo "$keluaran"
fi
if printf '%s' "$keluaran" | grep -q 'GAGAL'; then
ada_gagal=1
fi
done
if [ "$ada_gagal" -eq 1 ]; then
echo "SUIT REGRESI: GAGAL"
exit 1
fi
echo "SUIT REGRESI: LULUS"
laporan-stok.sql
Unduh berkas-- Laporan yang dilindungi golden file: format dan isinya dikunci
-- lewat hasil-ekspektasi.txt; perubahan apa pun di output akan terdeteksi.
SELECT kode,
nama,
stok,
stok_minimum,
CASE WHEN stok < stok_minimum THEN 'RESTOCK' ELSE 'OK' END AS status_stok
FROM barang
ORDER BY kode;