Function dan trigger adalah kode yang berjalan tersembunyi di dalam server — pemanggilnya bukan manusia, melainkan SELECT dan INSERT yang tidak tahu sedang mengeksekusi logika apa. Kesalahan di objek ini punya daya ledak khusus: function hitung_diskon yang salah menghitung merusak setiap transaksi yang lewat, dan trigger yang menyala di event yang salah merusak data diam-diam. Pengujian harus mencakup empat permukaan: keluaran untuk masukan normal, kasus batas, jalur RAISE EXCEPTION, dan efek samping trigger. Chapter ini menguji semuanya pada satu set objek yang realistis, lalu menutup dengan pola paling produktif di seluruh section ini: test harness data-driven.
Objek yang Akan Diuji #
Fixture berikut memuat tabel pesanan, function hitung_diskon, dan dua trigger — satu trigger audit yang mencatat perubahan status, satu trigger penyaring yang membuang pesanan bermasalah sebelum tersimpan:
-- Objek yang akan diuji: function hitung_diskon + dua trigger pada pesanan.
CREATE TABLE pesanan (
id integer PRIMARY KEY,
pelanggan text NOT NULL,
total numeric NOT NULL,
status text NOT NULL DEFAULT 'baru'
);
-- Tabel audit; kolom waktu sengaja tidak ikut di-SELECT pada contoh uji
-- karena nilainya berubah tiap eksekusi (tidak deterministik).
CREATE TABLE audit_pesanan (
id_pesanan integer NOT NULL,
aksi text NOT NULL,
status_lama text,
status_baru text NOT NULL,
waktu timestamptz NOT NULL DEFAULT now()
);
-- Function: hitung harga setelah diskon; diskon di luar 0..100 -> exception.
CREATE FUNCTION hitung_diskon(brutto numeric, persen_diskon numeric)
RETURNS numeric
LANGUAGE plpgsql
AS $$
BEGIN
IF brutto IS NULL OR persen_diskon IS NULL THEN
RETURN NULL;
END IF;
IF persen_diskon < 0 OR persen_diskon > 100 THEN
RAISE EXCEPTION 'persen diskon % di luar rentang 0..100', persen_diskon
USING ERRCODE = 'TMJ01';
END IF;
RETURN round(brutto * (100 - persen_diskon) / 100, 2);
END $$;
-- Trigger 1: audit perubahan status (INSERT atau UPDATE kolom status saja).
CREATE FUNCTION catat_audit() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO audit_pesanan (id_pesanan, aksi, status_lama, status_baru)
VALUES (NEW.id,
TG_OP,
CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE OLD.status END,
NEW.status);
RETURN NEW;
END $$;
CREATE TRIGGER audit_status
AFTER INSERT OR UPDATE OF status ON pesanan
FOR EACH ROW EXECUTE FUNCTION catat_audit();
-- Trigger 2: tolak pesanan bermasalah (total <= 0) secara diam-diam.
CREATE FUNCTION saring_pesanan() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.total <= 0 THEN
RETURN NULL; -- baris tidak pernah sampai ke tabel
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER saring_total
BEFORE INSERT ON pesanan
FOR EACH ROW EXECUTE FUNCTION saring_pesanan();Output yang Dihasilkan:
CREATE TABLE
CREATE TABLE
CREATE FUNCTION
CREATE FUNCTION
CREATE TRIGGER
CREATE FUNCTION
CREATE TRIGGERTiga perilaku yang nanti diuji satu per satu: hitung_diskon mengembalikan numeric hasil perhitungan dan melempar exception untuk diskon di luar rentang 0..100; trigger audit_status hanya menyala untuk INSERT atau UPDATE kolom status; dan trigger saring_total mengembalikan NULL untuk total tidak positif sehingga barisnya tidak pernah tersimpan. Dasar trigger itu sendiri sudah dibahas di chapter Konsep Trigger
.
Masukan Diketahui, Keluaran Diketahui #
Pengujian function dimulai dari bentuk paling sederhana: panggil dengan masukan yang hasilnya bisa dihitung tangan, bandingkan dengan CASE WHEN. Pola ini sama dengan asersi skalar di chapter pertama — hanya objeknya kini function, bukan query:
-- Uji function: masukan diketahui -> keluaran diketahui.
-- Cakup: kasus normal, batas bawah/atas, pembulatan, dan NULL.
SELECT CASE
WHEN hitung_diskon(100000, 10) = 90000
THEN 'LULUS: 100000 dengan diskon 10% menjadi 90000'
ELSE 'GAGAL: aktual ' || hitung_diskon(100000, 10)::text
END AS uji_normal;
SELECT CASE
WHEN hitung_diskon(100000, 0) = 100000
THEN 'LULUS: batas bawah diskon 0 -> harga tidak berubah'
ELSE 'GAGAL: aktual ' || hitung_diskon(100000, 0)::text
END AS uji_batas_bawah;
SELECT CASE
WHEN hitung_diskon(100000, 100) = 0
THEN 'LULUS: batas atas diskon 100 -> gratis'
ELSE 'GAGAL: aktual ' || hitung_diskon(100000, 100)::text
END AS uji_batas_atas;
SELECT CASE
WHEN hitung_diskon(99999, 12.5) = 87499.13
THEN 'LULUS: pembulatan dua desimal (87499.125 -> 87499.13)'
ELSE 'GAGAL: aktual ' || hitung_diskon(99999, 12.5)::text
END AS uji_pembulatan;
SELECT CASE
WHEN hitung_diskon(100000, NULL) IS NULL
THEN 'LULUS: persen NULL menghasilkan NULL, bukan error'
ELSE 'GAGAL: seharusnya NULL'
END AS uji_null;Output yang Dihasilkan:
uji_normal
-----------------------------------------------
LULUS: 100000 dengan diskon 10% menjadi 90000
(1 row)
uji_batas_bawah
----------------------------------------------------
LULUS: batas bawah diskon 0 -> harga tidak berubah
(1 row)
uji_batas_atas
----------------------------------------
LULUS: batas atas diskon 100 -> gratis
(1 row)
uji_pembulatan
-------------------------------------------------------
LULUS: pembulatan dua desimal (87499.125 -> 87499.13)
(1 row)
uji_null
---------------------------------------------------
LULUS: persen NULL menghasilkan NULL, bukan error
(1 row)Lima asersi tersebut sengaja memilih kasus yang berbeda-beda karakternya: satu kasus normal (diskon 10%), dua batas rentang (diskon 0 dan 100 — keduanya sah), satu kasus pembulatan (87499.125 yang harus naik menjadi 87499.13), dan satu kasus NULL. Kasus NULL penting untuk function yang dipakai di kolom nullable: keputusan “NULL masuk, NULL keluar” adalah kontrak yang harus dijaga, bukan kebetulan yang terjadi.
Menguji Jalur Exception #
Function yang tugasnya memvalidasi masukan hanya teruji bila jalur tolaknya benar-benar diuji — function validasi yang tidak pernah melempar error justru gagal total. Masalahnya: SELECT hitung_diskon(100000, 150) yang melempar exception akan menghentikan file uji. Solusinya adalah menangkap error di dalam DO block, pola yang dikenalkan di chapter Exception Handling
:
-- Uji jalur exception: function HARUS melempar error untuk masukan di luar
-- rentang. Error ditangkap DO block agar satu file bisa menguji dua kasus.
CREATE TABLE hasil_exception (
kasus text,
tertangkap boolean,
pesan text
);
DO $$
DECLARE
v_pesan text;
BEGIN
BEGIN
PERFORM hitung_diskon(100000, 150);
INSERT INTO hasil_exception VALUES ('diskon 150', false, 'tidak error (bug!)');
EXCEPTION WHEN SQLSTATE 'TMJ01' THEN
GET STACKED DIAGNOSTICS v_pesan = MESSAGE_TEXT;
INSERT INTO hasil_exception VALUES ('diskon 150', true, v_pesan);
END;
BEGIN
PERFORM hitung_diskon(100000, -5);
INSERT INTO hasil_exception VALUES ('diskon -5', false, 'tidak error (bug!)');
EXCEPTION WHEN SQLSTATE 'TMJ01' THEN
GET STACKED DIAGNOSTICS v_pesan = MESSAGE_TEXT;
INSERT INTO hasil_exception VALUES ('diskon -5', true, v_pesan);
END;
END $$;
SELECT kasus, tertangkap, pesan FROM hasil_exception ORDER BY kasus;
SELECT CASE
WHEN (SELECT count(*) FROM hasil_exception WHERE tertangkap) = 2
THEN 'LULUS: kedua masukan tidak sah sama-sama melempar exception'
ELSE 'GAGAL: ada masukan tidak sah yang lolos'
END AS uji_exception;Output yang Dihasilkan:
CREATE TABLE
DO
kasus | tertangkap | pesan
------------+------------+------------------------------------------
diskon -5 | t | persen diskon -5 di luar rentang 0..100
diskon 150 | t | persen diskon 150 di luar rentang 0..100
(2 rows)
uji_exception
-------------------------------------------------------------
LULUS: kedua masukan tidak sah sama-sama melempar exception
(1 row)Kedua masukan tidak sah (150 dan -5) tertangkap dengan SQLSTATE 'TMJ01' — kode custom yang dipasang function lewat USING ERRCODE. Menangkap berdasarkan kode membuat uji ini presisi: bila function kelak diubah melempar error lain (misalnya kode default P0001), uji ini otomatis gagal dan perubahan kontrak itu tertangkap. Baris 'tidak error (bug!)' hanya akan muncul bila INSERT pengujian lolos — yaitu tepat saat function kehilangan kemampuannya menolak.
Menguji Efek Samping Trigger #
Trigger tidak punya nilai kembalian yang bisa dibandingkan — yang bisa diverifikasi adalah efek sampingnya di tabel lain. Uji trigger audit menjawab tiga pertanyaan berurutan: apakah INSERT menghasilkan baris audit, apakah UPDATE kolom status menambah baris audit, dan — sama pentingnya — apakah UPDATE kolom lain dibiarkan tidak menyala:
-- Uji trigger audit: efek samping terverifikasi, dan trigger hanya menyala
-- untuk event yang benar (UPDATE kolom status, bukan kolom lain).
INSERT INTO pesanan (id, pelanggan, total, status)
VALUES (1, 'Siti Rahayu', 250000, 'baru');
-- Setelah INSERT: harus ada tepat satu baris audit untuk pesanan 1.
SELECT id_pesanan, aksi, status_lama, status_baru
FROM audit_pesanan
ORDER BY id_pesanan, aksi;
-- UPDATE kolom status: memicu trigger, baris audit bertambah satu.
UPDATE pesanan SET status = 'dibayar' WHERE id = 1;
SELECT id_pesanan, aksi, status_lama, status_baru
FROM audit_pesanan
ORDER BY id_pesanan, aksi;
-- UPDATE kolom LAIN (total): trigger UPDATE OF status tidak boleh menyala.
UPDATE pesanan SET total = 260000 WHERE id = 1;
SELECT CASE
WHEN (SELECT count(*) FROM audit_pesanan) = 2
THEN 'LULUS: INSERT dan UPDATE status tercatat; UPDATE total diabaikan'
ELSE 'GAGAL: ekspektasi 2 baris audit, aktual '
|| (SELECT count(*) FROM audit_pesanan)::text
END AS uji_event_trigger;Output yang Dihasilkan:
INSERT 0 1
id_pesanan | aksi | status_lama | status_baru
------------+--------+-------------+-------------
1 | INSERT | | baru
(1 row)
UPDATE 1
id_pesanan | aksi | status_lama | status_baru
------------+--------+-------------+-------------
1 | INSERT | | baru
1 | UPDATE | baru | dibayar
(2 rows)
UPDATE 1
uji_event_trigger
------------------------------------------------------------------
LULUS: INSERT dan UPDATE status tercatat; UPDATE total diabaikan
(1 row)Dua SELECT pertama memperlihatkan pertumbuhan tabel audit: satu baris INSERT (dengan status_lama kosong — trigger tahu ini insert), lalu satu baris UPDATE dengan status_lama terisi. Pertanyaan ketiga dijawab lewat hitungan: setelah UPDATE pesanan SET total = 260000, jumlah baris audit tetap 2 — klausa UPDATE OF status membuat trigger hanya menyala saat kolom yang disebut ikut berubah. Tanpa uji ini, trigger yang salah tulis AFTER UPDATE (tanpa OF) akan mencatat setiap perubahan minor dan menggembungkan tabel audit di produksi.
RETURN NULL: Baris yang Dilewati Diam-Diam #
Trigger BEFORE yang mengembalikan NULL membatalkan operasi tanpa error — perilaku yang berguna untuk menyaring data kotor, tetapi berbahaya bila tidak teruji karena tidak ada pesan apa pun yang memberi tahu barisnya hilang:
-- Uji trigger BEFORE dengan RETURN NULL: baris yang ditolak tidak tersimpan
-- dan tidak meninggalkan jejak audit sama sekali.
INSERT INTO pesanan (id, pelanggan, total, status)
VALUES (2, 'Budi Santoso', 175000, 'baru');
-- total negatif: trigger saring_total mengembalikan NULL -> baris dilewati.
INSERT INTO pesanan (id, pelanggan, total, status)
VALUES (3, 'Data Uji Kotor', -5000, 'baru');
SELECT id, pelanggan, total, status FROM pesanan ORDER BY id;
SELECT CASE
WHEN (SELECT count(*) FROM pesanan) = 2
THEN 'LULUS: pesanan 3 dengan total negatif tidak pernah tersimpan'
ELSE 'GAGAL: ekspektasi 2 baris, aktual '
|| (SELECT count(*) FROM pesanan)::text
END AS uji_skip_pesan;
SELECT CASE
WHEN (SELECT count(*) FROM audit_pesanan WHERE id_pesanan = 3) = 0
THEN 'LULUS: baris yang dilewati tidak meninggalkan jejak audit'
ELSE 'GAGAL: ada baris audit untuk pesanan yang tidak tersimpan'
END AS uji_skip_audit;Output yang Dihasilkan:
INSERT 0 1
INSERT 0 0
id | pelanggan | total | status
----+--------------+--------+---------
1 | Siti Rahayu | 260000 | dibayar
2 | Budi Santoso | 175000 | baru
(2 rows)
uji_skip_pesan
--------------------------------------------------------------
LULUS: pesanan 3 dengan total negatif tidak pernah tersimpan
(1 row)
uji_skip_audit
-----------------------------------------------------------
LULUS: baris yang dilewati tidak meninggalkan jejak audit
(1 row)Bukti terkuatnya ada di tag psql: INSERT 0 1 untuk pesanan Budi, lalu INSERT 0 0 untuk pesanan dengan total negatif — nol baris tersimpan, tanpa error, tanpa peringatan. Dua asersi penutup memastikan pesanan kotor itu tidak ada di pesanan dan tidak meninggalkan jejak audit — kombinasi yang penting: bila baris yang dilewati ternyata tetap tercatat di audit, laporan audit Anda menghitung pesanan yang tidak pernah ada.
Test Harness Data-Driven #
Pola-pola di atas bagus untuk satu-dua kasus, tetapi function nyata punya lusinan kombinasi masukan. Menulis satu SELECT CASE per kasus membuat file uji membengkak tanpa nilai tambah. Solusinya: pindahkan kasus uji ke tabel, lalu jalankan function untuk semuanya dengan satu query — inilah test harness data-driven:
-- Test harness data-driven: tabel kasus uji berisi (masukan, ekspektasi),
-- satu query menjalankan function untuk SEMUA kasus dan melaporkan selisih.
-- Baris terakhir sengaja diberi ekspektasi salah untuk memperagakan deteksi.
CREATE TABLE kasus_uji (
kasus text PRIMARY KEY,
brutto numeric,
persen numeric,
ekspektasi numeric
);
INSERT INTO kasus_uji (kasus, brutto, persen, ekspektasi) VALUES
('batas-atas-diskon', 100000, 100, 0),
('batas-bawah-diskon', 100000, 0, 100000),
('brutto-NULL', NULL, 10, NULL),
('ekspektasi-salah', 100000, 25, 74000),
('normal', 100000, 10, 90000),
('pembulatan', 99999, 12.5, 87499.13),
('persen-NULL', 100000, NULL, NULL);
SELECT k.kasus,
CASE
WHEN hitung_diskon(k.brutto, k.persen) IS NOT DISTINCT FROM k.ekspektasi
THEN 'LULUS'
ELSE 'GAGAL: aktual '
|| COALESCE(hitung_diskon(k.brutto, k.persen)::text, 'NULL')
|| ', ekspektasi ' || COALESCE(k.ekspektasi::text, 'NULL')
END AS status
FROM kasus_uji k
ORDER BY k.kasus;
-- Ringkasan: satu angka untuk pipeline CI yang membaca.
SELECT CASE
WHEN count(*) FILTER (WHERE status = 'GAGAL') = 0
THEN 'LULUS: semua kasus lolos'
ELSE 'GAGAL: ' || count(*) FILTER (WHERE status = 'GAGAL')
|| ' dari ' || count(*) || ' kasus gagal'
END AS ringkasan
FROM (
SELECT CASE
WHEN hitung_diskon(k.brutto, k.persen) IS NOT DISTINCT FROM k.ekspektasi
THEN 'LULUS' ELSE 'GAGAL'
END AS status
FROM kasus_uji k
) laporan;Output yang Dihasilkan:
CREATE TABLE
INSERT 0 7
kasus | status
--------------------+------------------------------------------
batas-atas-diskon | LULUS
batas-bawah-diskon | LULUS
brutto-NULL | LULUS
ekspektasi-salah | GAGAL: aktual 75000.00, ekspektasi 74000
normal | LULUS
pembulatan | LULUS
persen-NULL | LULUS
(7 rows)
ringkasan
-----------------------------
GAGAL: 1 dari 7 kasus gagal
(1 row)Bedah komponen kuncinya:
- Tabel
kasus_ujiberisi (masukan, ekspektasi) — menambah kasus uji baru cukup satuINSERT, tanpa menyentuh logika pengujian. Ekspektasi dan masukan hidup berdampingan sehingga tabelnya terbaca seperti spesifikasi. IS NOT DISTINCT FROM, bukan=— perbandingan=terhadap ekspektasiNULLselalu menghasilkan NULL (tidak lulus, tidak gagal — menggantung).IS NOT DISTINCT FROMmemperlakukan NULL sebagai nilai yang bisa dicocokkan, sehingga kasus NULL teruji seperti kasus lain.- Pesan GAGAL membawa nilai aktual dan ekspektasi — lihat baris
ekspektasi-salah:aktual 75000.00, ekspektasi 74000. Kasus ini sengaja dipasang salah untuk membuktikan harness benar-benar mendeteksi kegagalan; tanpa kemampuan gagal, pengujian apa pun hanya teater. - Ringkasan satu angka — query kedua merangkum
N dari M kasus gagal, format yang siap dibaca pipeline CI: satu string, keputusan lulus/tidak langsung terbaca.
Pola harness ini — kasus di tabel, eksekusi massal, laporan ringkas — adalah benih dari regression suite yang sesungguhnya: kumpulan uji yang dijalankan ulang otomatis setiap kali skema atau kode berubah. Chapter berikutnya membangun suite itu lengkap dengan pustaka asersi, skrip runner, dan golden file.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-objek-uji.sql
Unduh berkas-- Objek yang akan diuji: function hitung_diskon + dua trigger pada pesanan.
CREATE TABLE pesanan (
id integer PRIMARY KEY,
pelanggan text NOT NULL,
total numeric NOT NULL,
status text NOT NULL DEFAULT 'baru'
);
-- Tabel audit; kolom waktu sengaja tidak ikut di-SELECT pada contoh uji
-- karena nilainya berubah tiap eksekusi (tidak deterministik).
CREATE TABLE audit_pesanan (
id_pesanan integer NOT NULL,
aksi text NOT NULL,
status_lama text,
status_baru text NOT NULL,
waktu timestamptz NOT NULL DEFAULT now()
);
-- Function: hitung harga setelah diskon; diskon di luar 0..100 -> exception.
CREATE FUNCTION hitung_diskon(brutto numeric, persen_diskon numeric)
RETURNS numeric
LANGUAGE plpgsql
AS $$
BEGIN
IF brutto IS NULL OR persen_diskon IS NULL THEN
RETURN NULL;
END IF;
IF persen_diskon < 0 OR persen_diskon > 100 THEN
RAISE EXCEPTION 'persen diskon % di luar rentang 0..100', persen_diskon
USING ERRCODE = 'TMJ01';
END IF;
RETURN round(brutto * (100 - persen_diskon) / 100, 2);
END $$;
-- Trigger 1: audit perubahan status (INSERT atau UPDATE kolom status saja).
CREATE FUNCTION catat_audit() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO audit_pesanan (id_pesanan, aksi, status_lama, status_baru)
VALUES (NEW.id,
TG_OP,
CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE OLD.status END,
NEW.status);
RETURN NEW;
END $$;
CREATE TRIGGER audit_status
AFTER INSERT OR UPDATE OF status ON pesanan
FOR EACH ROW EXECUTE FUNCTION catat_audit();
-- Trigger 2: tolak pesanan bermasalah (total <= 0) secara diam-diam.
CREATE FUNCTION saring_pesanan() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.total <= 0 THEN
RETURN NULL; -- baris tidak pernah sampai ke tabel
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER saring_total
BEFORE INSERT ON pesanan
FOR EACH ROW EXECUTE FUNCTION saring_pesanan();
uji-exception.sql
Unduh berkas-- Uji jalur exception: function HARUS melempar error untuk masukan di luar
-- rentang. Error ditangkap DO block agar satu file bisa menguji dua kasus.
CREATE TABLE hasil_exception (
kasus text,
tertangkap boolean,
pesan text
);
DO $$
DECLARE
v_pesan text;
BEGIN
BEGIN
PERFORM hitung_diskon(100000, 150);
INSERT INTO hasil_exception VALUES ('diskon 150', false, 'tidak error (bug!)');
EXCEPTION WHEN SQLSTATE 'TMJ01' THEN
GET STACKED DIAGNOSTICS v_pesan = MESSAGE_TEXT;
INSERT INTO hasil_exception VALUES ('diskon 150', true, v_pesan);
END;
BEGIN
PERFORM hitung_diskon(100000, -5);
INSERT INTO hasil_exception VALUES ('diskon -5', false, 'tidak error (bug!)');
EXCEPTION WHEN SQLSTATE 'TMJ01' THEN
GET STACKED DIAGNOSTICS v_pesan = MESSAGE_TEXT;
INSERT INTO hasil_exception VALUES ('diskon -5', true, v_pesan);
END;
END $$;
SELECT kasus, tertangkap, pesan FROM hasil_exception ORDER BY kasus;
SELECT CASE
WHEN (SELECT count(*) FROM hasil_exception WHERE tertangkap) = 2
THEN 'LULUS: kedua masukan tidak sah sama-sama melempar exception'
ELSE 'GAGAL: ada masukan tidak sah yang lolos'
END AS uji_exception;
uji-function.sql
Unduh berkas-- Uji function: masukan diketahui -> keluaran diketahui.
-- Cakup: kasus normal, batas bawah/atas, pembulatan, dan NULL.
SELECT CASE
WHEN hitung_diskon(100000, 10) = 90000
THEN 'LULUS: 100000 dengan diskon 10% menjadi 90000'
ELSE 'GAGAL: aktual ' || hitung_diskon(100000, 10)::text
END AS uji_normal;
SELECT CASE
WHEN hitung_diskon(100000, 0) = 100000
THEN 'LULUS: batas bawah diskon 0 -> harga tidak berubah'
ELSE 'GAGAL: aktual ' || hitung_diskon(100000, 0)::text
END AS uji_batas_bawah;
SELECT CASE
WHEN hitung_diskon(100000, 100) = 0
THEN 'LULUS: batas atas diskon 100 -> gratis'
ELSE 'GAGAL: aktual ' || hitung_diskon(100000, 100)::text
END AS uji_batas_atas;
SELECT CASE
WHEN hitung_diskon(99999, 12.5) = 87499.13
THEN 'LULUS: pembulatan dua desimal (87499.125 -> 87499.13)'
ELSE 'GAGAL: aktual ' || hitung_diskon(99999, 12.5)::text
END AS uji_pembulatan;
SELECT CASE
WHEN hitung_diskon(100000, NULL) IS NULL
THEN 'LULUS: persen NULL menghasilkan NULL, bukan error'
ELSE 'GAGAL: seharusnya NULL'
END AS uji_null;
uji-harness.sql
Unduh berkas-- Test harness data-driven: tabel kasus uji berisi (masukan, ekspektasi),
-- satu query menjalankan function untuk SEMUA kasus dan melaporkan selisih.
-- Baris terakhir sengaja diberi ekspektasi salah untuk memperagakan deteksi.
CREATE TABLE kasus_uji (
kasus text PRIMARY KEY,
brutto numeric,
persen numeric,
ekspektasi numeric
);
INSERT INTO kasus_uji (kasus, brutto, persen, ekspektasi) VALUES
('batas-atas-diskon', 100000, 100, 0),
('batas-bawah-diskon', 100000, 0, 100000),
('brutto-NULL', NULL, 10, NULL),
('ekspektasi-salah', 100000, 25, 74000),
('normal', 100000, 10, 90000),
('pembulatan', 99999, 12.5, 87499.13),
('persen-NULL', 100000, NULL, NULL);
SELECT k.kasus,
CASE
WHEN hitung_diskon(k.brutto, k.persen) IS NOT DISTINCT FROM k.ekspektasi
THEN 'LULUS'
ELSE 'GAGAL: aktual '
|| COALESCE(hitung_diskon(k.brutto, k.persen)::text, 'NULL')
|| ', ekspektasi ' || COALESCE(k.ekspektasi::text, 'NULL')
END AS status
FROM kasus_uji k
ORDER BY k.kasus;
-- Ringkasan: satu angka untuk pipeline CI yang membaca.
SELECT CASE
WHEN count(*) FILTER (WHERE status = 'GAGAL') = 0
THEN 'LULUS: semua kasus lolos'
ELSE 'GAGAL: ' || count(*) FILTER (WHERE status = 'GAGAL')
|| ' dari ' || count(*) || ' kasus gagal'
END AS ringkasan
FROM (
SELECT CASE
WHEN hitung_diskon(k.brutto, k.persen) IS NOT DISTINCT FROM k.ekspektasi
THEN 'LULUS' ELSE 'GAGAL'
END AS status
FROM kasus_uji k
) laporan;
uji-trigger-audit.sql
Unduh berkas-- Uji trigger audit: efek samping terverifikasi, dan trigger hanya menyala
-- untuk event yang benar (UPDATE kolom status, bukan kolom lain).
INSERT INTO pesanan (id, pelanggan, total, status)
VALUES (1, 'Siti Rahayu', 250000, 'baru');
-- Setelah INSERT: harus ada tepat satu baris audit untuk pesanan 1.
SELECT id_pesanan, aksi, status_lama, status_baru
FROM audit_pesanan
ORDER BY id_pesanan, aksi;
-- UPDATE kolom status: memicu trigger, baris audit bertambah satu.
UPDATE pesanan SET status = 'dibayar' WHERE id = 1;
SELECT id_pesanan, aksi, status_lama, status_baru
FROM audit_pesanan
ORDER BY id_pesanan, aksi;
-- UPDATE kolom LAIN (total): trigger UPDATE OF status tidak boleh menyala.
UPDATE pesanan SET total = 260000 WHERE id = 1;
SELECT CASE
WHEN (SELECT count(*) FROM audit_pesanan) = 2
THEN 'LULUS: INSERT dan UPDATE status tercatat; UPDATE total diabaikan'
ELSE 'GAGAL: ekspektasi 2 baris audit, aktual '
|| (SELECT count(*) FROM audit_pesanan)::text
END AS uji_event_trigger;
uji-trigger-skip.sql
Unduh berkas-- Uji trigger BEFORE dengan RETURN NULL: baris yang ditolak tidak tersimpan
-- dan tidak meninggalkan jejak audit sama sekali.
INSERT INTO pesanan (id, pelanggan, total, status)
VALUES (2, 'Budi Santoso', 175000, 'baru');
-- total negatif: trigger saring_total mengembalikan NULL -> baris dilewati.
INSERT INTO pesanan (id, pelanggan, total, status)
VALUES (3, 'Data Uji Kotor', -5000, 'baru');
SELECT id, pelanggan, total, status FROM pesanan ORDER BY id;
SELECT CASE
WHEN (SELECT count(*) FROM pesanan) = 2
THEN 'LULUS: pesanan 3 dengan total negatif tidak pernah tersimpan'
ELSE 'GAGAL: ekspektasi 2 baris, aktual '
|| (SELECT count(*) FROM pesanan)::text
END AS uji_skip_pesan;
SELECT CASE
WHEN (SELECT count(*) FROM audit_pesanan WHERE id_pesanan = 3) = 0
THEN 'LULUS: baris yang dilewati tidak meninggalkan jejak audit'
ELSE 'GAGAL: ada baris audit untuk pesanan yang tidak tersimpan'
END AS uji_skip_audit;