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 TABLE yang “tak berbahaya” bisa mengubah urutan kolom SELECT *, 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:

sql assert-lib.sql
-- 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:

text
CREATE FUNCTION
CREATE FUNCTION

Bedah desainnya:

  1. anyelement menerima tipe apa saja — satu function untuk integer, text, numeric, tanggal; tipe aktual dan ekspektasi harus cocok (panggil dengan cast eksplisit seperti 5::bigint bila perlu).
  2. IS NOT DISTINCT FROM sebagai pembanding — kasus NULL tercakup otomatis; NULL dibandingkan NULL dianggap lulus.
  3. Pesan gagal memuat kedua nilai — aktual=... ekspektasi=... — sehingga log CI langsung menunjukkan seberapa jauh meleset.
  4. assert_set_equal membungkus pola EXCEPT dua arah dari chapter Menguji Query SQL dalam EXECUTE — 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:

text
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:

text
==> 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: LULUS

Mekanismenya tiga lapis:

  1. ON_ERROR_STOP=1 — error SQL apa pun menghentikan file dan membuat psql keluar kode 3; runner menangkapnya lewat $? dan langsung gagal.
  2. grep -q 'GAGAL' — asersi yang lulus sql-nya tetapi gagal logikanya (baris GAGAL - ...) terdeteksi dari teksnya.
  3. exit 1 pada akhir — suite yang gagal memberi kode keluar non-zero ke pemanggilnya, sehingga make 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:

text
  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:

text
  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:

bash cek-golden.sh
#!/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

Output yang Dihasilkan:

text
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:

sql
-- 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:

yaml
# 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.sh

Langkah 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
sql
-- 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
sql
-- 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
bash
#!/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
bash
#!/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
sql
-- 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;