Topik ini membangun pengujian untuk mesin konfigurasi Pencatat, lengkap dengan kerangka uji (test harness) yang dibuat sendiri. Tujuan pengujian bukan sekadar membuktikan kode berjalan, melainkan mengunci perilaku fail-safe mesin, yaitu pelanggaran dilaporkan, nilai bawaan digunakan, dan error terstruktur dikembalikan, sehingga perubahan berikutnya pada mesin dapat diverifikasi ulang dengan cepat.
Kenapa kerangka uji dibuat sendiri#
Lua tidak menyertakan kerangka pengujian dalam pustaka standarnya, dan proyek tersemat menambah alasan yang lebih kuat untuk tidak memasang dependensi eksternal. Skrip yang berjalan di dalam host sering kali dilarang memuat pustaka pihak ketiga, sebagaimana skrip Redis yang hanya dapat memanggil API yang disediakan hostnya. Kerangka uji untuk proyek semacam ini pun harus berupa kode Lua polos yang berjalan dengan penerjemah mandiri maupun di dalam sandbox host.
Kerangka yang dibuat sendiri juga cukup kecil untuk dibaca sekaligus. Asersi standar Lua, yaitu assert(nilai, pesan), mengangkat error bila nilai yang diperiksa bernilai nil atau false, sehingga bentuknya berguna untuk pemeriksaan sederhana. Namun, kerangka proyek memerlukan perilaku yang lebih spesifik, yaitu asersi kesamaan yang mampu membandingkan table per kunci dan melaporkan kunci yang berbeda tanpa menampilkan table mentah. Kebutuhan inilah yang mendorong pembuatan asersi lokal.
Kerangka harness#
Berkas uji-mesin.lua memuat mesin, mendefinisikan kerangkanya, lalu menjalankan seluruh kasus uji di akhir berkas. Salinan mesin.lua versi terbaru menyertai berkas ini agar pengujian berdiri sendiri.
-- uji-mesin.lua
-- Test harness sederhana untuk mesin konfigurasi aplikasi Pencatat.
local mesin = dofile("mesin.lua")
local jumlah = 0
local lulus = 0
local gagal = 0
-- Menjalankan satu kasus uji secara terlindungi dan mencatat statusnya.
local function uji(nama, fungsi_uji)
jumlah = jumlah + 1
local ok, pesan = pcall(fungsi_uji)
if ok then
lulus = lulus + 1
print("LULUS " .. nama)
else
gagal = gagal + 1
print("GAGAL " .. nama)
print(" " .. tostring(pesan))
end
end
-- Asersi lokal, yaitu mengangkat error level 0 bila kedua nilai berbeda.
-- Table sederhana dibandingkan per kunci secara berulang.
local function asersi_sama(nilai, harapan)
if type(nilai) == "table" and type(harapan) == "table" then
for kunci, isi in pairs(harapan) do
asersi_sama(nilai[kunci], isi)
end
for kunci in pairs(nilai) do
if harapan[kunci] == nil then
error("kunci " .. tostring(kunci) .. " tidak diharapkan", 0)
end
end
return
end
if nilai ~= harapan then
error(string.format("nilai %s seharusnya %s",
tostring(nilai), tostring(harapan)), 0)
end
end
-- Kode kegagalan pada teks pelanggaran pertama dari daftar terurut.
local function kode_pertama(pelanggaran)
return pelanggaran[1]:match("^[A-Z_]+")
end
-- Kasus uji: seluruh kunci sah diterima apa adanya.
uji("konfigurasi normal penuh", function()
local final, pelanggaran = mesin.muat_konfigurasi({
tema = { nama = "terang", warna = "#f7f7f7", ukuran_teks = 16 },
aturan = { batas_panjang = 5000, pola_berkas = "catatan%d+" },
konten = { label = "Catatan Harian" },
})
asersi_sama(#pelanggaran, 0)
asersi_sama(final, {
tema = { nama = "terang", warna = "#f7f7f7", ukuran_teks = 16 },
aturan = { batas_panjang = 5000, pola_berkas = "catatan%d+" },
konten = { label = "Catatan Harian" },
})
end)
-- Kasus uji: kunci di dalam grup tema yang tidak ada pada skema dilaporkan.
uji("kunci tema tidak dikenal", function()
local final, pelanggaran = mesin.muat_konfigurasi({
tema = { nama = "gelap", latar = "#222222" },
})
asersi_sama(#pelanggaran, 1)
asersi_sama(kode_pertama(pelanggaran), "KUNCI_TAK_DIKENAL")
asersi_sama(final.tema.nama, "gelap")
end)
-- Kasus uji: nilai di bawah batas bawah digantikan default.
uji("ukuran_teks terlalu kecil", function()
local final, pelanggaran = mesin.muat_konfigurasi({
tema = { ukuran_teks = 5 },
})
asersi_sama(#pelanggaran, 1)
asersi_sama(kode_pertama(pelanggaran), "NILAI_DI_LUAR_BATAS")
asersi_sama(final.tema.ukuran_teks, 14)
end)
-- Kasus uji: tipe yang salah digantikan default.
uji("tipe salah pada batas_panjang", function()
local final, pelanggaran = mesin.muat_konfigurasi({
aturan = { batas_panjang = "seribu" },
})
asersi_sama(#pelanggaran, 1)
asersi_sama(kode_pertama(pelanggaran), "TIPE_SALAH")
asersi_sama(final.aturan.batas_panjang, 1000)
end)
-- Kasus uji: kandidat kosong menghasilkan seluruh nilai default.
uji("kandidat kosong", function()
local final, pelanggaran = mesin.muat_konfigurasi({})
asersi_sama(#pelanggaran, 0)
asersi_sama(final, {
tema = { nama = "gelap", warna = "#1e1e2e", ukuran_teks = 14 },
aturan = { batas_panjang = 1000, pola_berkas = "catatan%d+" },
konten = { label = "Catatan" },
})
end)
-- Kasus uji: muat_aman menangkap kandidat nil dan melaporkan error terstruktur.
uji("muat_aman menangkap kandidat nil", function()
local sukses, hasil = mesin.muat_aman(nil)
asersi_sama(sukses, false)
asersi_sama(hasil.kode, "KANDIDAT_TIDAK_SAH")
asersi_sama(type(hasil.pesan), "string")
end)
print(string.format("ringkasan: %d lulus, %d gagal dari %d uji",
lulus, gagal, jumlah))
os.exit(gagal == 0 and 0 or 1)LULUS konfigurasi normal penuh
LULUS kunci tema tidak dikenal
LULUS ukuran_teks terlalu kecil
LULUS tipe salah pada batas_panjang
LULUS kandidat kosong
LULUS muat_aman menangkap kandidat nil
ringkasan: 6 lulus, 0 gagal dari 6 uji-- mesin.lua
-- Inti mesin konfigurasi aplikasi Pencatat.
-- Versi ini menambahkan muat_aman, yaitu pelaksanaan terlindungi di tepi
-- lapisan skrip. Perilaku muat_konfigurasi dan tampilkan tidak berubah.
local mesin = {}
-- Skema konfigurasi, yaitu tiga grup kunci sah beserta tipe, default, dan batas.
local SKEMA = {
tema = {
nama = { tipe = "string", default = "gelap" },
warna = { tipe = "string", default = "#1e1e2e" },
ukuran_teks = { tipe = "integer", default = 14, minimum = 8, maksimum = 32 },
},
aturan = {
batas_panjang = { tipe = "integer", default = 1000, minimum = 1, maksimum = 100000 },
pola_berkas = { tipe = "string", default = "catatan%d+" },
},
konten = {
label = { tipe = "string", default = "Catatan" },
},
}
-- Nama tipe yang ditampilkan pada pesan pelanggaran.
local function nama_tipe(tipe)
if tipe == "integer" then
return "bilangan bulat"
end
return tipe
end
-- Nama tipe aktual dari sebuah nilai untuk keperluan pesan pelanggaran.
local function nama_tipe_nilai(nilai)
local t = type(nilai)
if t == "number" then
return math.type(nilai) == "integer" and "bilangan bulat" or "bilangan pecahan"
end
return t
end
-- Memeriksa kesesuaian tipe nilai terhadap aturan skemanya.
local function tipe_sesuai(aturan, nilai)
if aturan.tipe == "integer" then
return type(nilai) == "number" and math.type(nilai) == "integer"
end
return type(nilai) == aturan.tipe
end
-- Memvalidasi satu nilai dan mengembalikan teks pelanggaran atau nil.
local function validasi_kunci(jalur, aturan, nilai)
if not tipe_sesuai(aturan, nilai) then
return string.format(
"TIPE_SALAH: kunci %s seharusnya bertipe %s, tetapi bertipe %s",
jalur, nama_tipe(aturan.tipe), nama_tipe_nilai(nilai)
)
end
if aturan.tipe == "integer" and (nilai < aturan.minimum or nilai > aturan.maksimum) then
return string.format(
"NILAI_DI_LUAR_BATAS: kunci %s seharusnya bernilai antara %d dan %d",
jalur, aturan.minimum, aturan.maksimum
)
end
return nil
end
-- Menggabungkan nilai default skema dengan kandidat konfigurasi.
-- Kandidat tidak pernah dimodifikasi, hasilnya selalu table baru.
-- Kandidat yang bukan table mengangkat error terstruktur dengan level 0.
function mesin.muat_konfigurasi(kandidat)
if type(kandidat) ~= "table" then
error({ kode = "KANDIDAT_TIDAK_SAH",
pesan = string.format(
"kandidat konfigurasi seharusnya table, tetapi bertipe %s", type(kandidat)
) }, 0)
end
local final = {}
local pelanggaran = {}
for nama_grup, skema_grup in pairs(SKEMA) do
final[nama_grup] = {}
local grup = kandidat[nama_grup]
if grup ~= nil and type(grup) ~= "table" then
table.insert(pelanggaran, string.format(
"TIPE_SALAH: grup %s seharusnya table, tetapi bertipe %s",
nama_grup, nama_tipe_nilai(grup)))
grup = nil
end
for nama_kunci, aturan in pairs(skema_grup) do
local nilai = nil
if grup ~= nil then
nilai = grup[nama_kunci]
end
if nilai == nil then
final[nama_grup][nama_kunci] = aturan.default
else
local temuan = validasi_kunci(nama_grup .. "." .. nama_kunci, aturan, nilai)
if temuan == nil then
final[nama_grup][nama_kunci] = nilai
else
table.insert(pelanggaran, temuan)
final[nama_grup][nama_kunci] = aturan.default
end
end
end
end
for nama_grup, grup in pairs(kandidat) do
if SKEMA[nama_grup] == nil then
table.insert(pelanggaran, string.format(
"KUNCI_TAK_DIKENAL: grup %s tidak dikenal", nama_grup))
elseif type(grup) == "table" then
for nama_kunci in pairs(grup) do
if SKEMA[nama_grup][nama_kunci] == nil then
table.insert(pelanggaran, string.format(
"KUNCI_TAK_DIKENAL: kunci %s.%s tidak dikenal", nama_grup, nama_kunci))
end
end
end
end
table.sort(pelanggaran)
return final, pelanggaran
end
-- Membungkus muat_konfigurasi dengan pcall agar error tidak menyebar ke host.
-- Bila sukses, hasil berupa table konfigurasi final, sedangkan bila gagal
-- hasil berupa table error terstruktur { kode = "...", pesan = "..." }.
function mesin.muat_aman(kandidat)
local ok, hasil = pcall(mesin.muat_konfigurasi, kandidat)
if ok then
return true, hasil
end
if type(hasil) == "table" and hasil.kode ~= nil then
return false, hasil
end
return false, { kode = "ERROR_TAK_TERDUGA", pesan = tostring(hasil) }
end
-- Mencetak laporan ringkas konfigurasi final dengan urutan yang tetap.
function mesin.tampilkan(final)
print(string.format("%-22s %s", "tema.nama", final.tema.nama))
print(string.format("%-22s %s", "tema.warna", final.tema.warna))
print(string.format("%-22s %s", "tema.ukuran_teks", final.tema.ukuran_teks))
print(string.format("%-22s %s", "aturan.batas_panjang", final.aturan.batas_panjang))
print(string.format("%-22s %s", "aturan.pola_berkas", final.aturan.pola_berkas))
print(string.format("%-22s %s", "konten.label", final.konten.label))
end
return mesinTiga bagian kerangka menentukan bentuk pengujian. Fungsi uji(nama, fungsi_uji) menjalankan satu kasus di dalam pcall, menaikkan penghitungnya, dan mencetak status LULUS atau GAGAL beserta pesan errornya. Fungsi asersi_sama(nilai, harapan) membandingkan nilai tunggal maupun table sederhana per kunci secara berulang dan mengangkat error level 0, sehingga pesannya tidak memuat prefiks posisi berkas yang tidak stabil. Fungsi kode_pertama(pelanggaran) mengambil kode kegagalan dari teks pelanggaran pertama yang terurut, karena mesin menjamin urutan daftar pelanggarannya melalui table.sort.
Baris terakhir berkas menggunakan os.exit(gagal == 0 and 0 or 1), sehingga kode keluar pengujian menyatakan keberhasilannya secara eksplisit. Konvensi ini memungkinkan pengujian dijalankan oleh alat otomatis, sebagaimana kode keluar menjadi kontrak standar alat uji pada umumnya.
Kasus uji yang dipilih#
Enam kasus uji menjaga perilaku yang paling menentukan. Kasus konfigurasi normal penuh memastikan kandidat sah diterima apa adanya tanpa satu pun pelanggaran. Kasus kunci tema tidak dikenal memastikan kunci di luar skema dilaporkan dengan kode KUNCI_TAK_DIKENAL dan tidak menggagalkan kunci lainnya. Kasus ukuran_teks terlalu kecil dan tipe salah pada batas_panjang mengunci dua kategori pelanggaran, yaitu NILAI_DI_LUAR_BATAS dan TIPE_SALAH, beserta penggantian nilainya dengan nilai bawaan. Kasus kandidat kosong menegaskan bahwa table kosong menghasilkan seluruh nilai bawaan tanpa pelanggaran. Kasus muat_aman menangkap kandidat nil menguji jalur kegagalan yang mengangkat error terstruktur, yaitu status false dengan kode KANDIDAT_TIDAK_SAH.
Pemilihan kasus mengikuti prinsip menguji kontrak, bukan menguji implementasi. Setiap kasus menyatakan perilaku yang dijanjikan mesin kepada penggunanya, sehingga implementasi mesin boleh ditulis ulang selama kasusnya tetap lulus.
Menjalankan pengujian#
Pengujian dijalankan dengan satu perintah berikut, dan keluarannya persis seperti blok Keluaran di bawahnya.
# jalankan seluruh kasus uji mesin
lua uji-mesin.luaLULUS konfigurasi normal penuh
LULUS kunci tema tidak dikenal
LULUS ukuran_teks terlalu kecil
LULUS tipe salah pada batas_panjang
LULUS kandidat kosong
LULUS muat_aman menangkap kandidat nil
ringkasan: 6 lulus, 0 gagal dari 6 ujiKeluaran menunjukkan enam kasus lulus tanpa kegagalan, disusul ringkasan yang memuat jumlah lulus, jumlah gagal, dan total kasus. Bila salah satu kasus gagal, baris GAGAL beserta pesan errornya muncul sebelum ringkasan, dan kode keluar berkas berubah menjadi bukan nol. Cara membaca keluaran semacam ini cepat, karena ringkasan baris terakhirnya sudah cukup untuk menilai kondisi mesin secara keseluruhan.
Kesalahan umum#
Kesalahan pertama adalah menguji detail implementasi alih-alih kontrak. Kasus uji yang membandingkan urutan iterasi internal atau bentuk pesan yang tidak dijanjikan akan rusak setiap kali implementasi ditulis ulang, meskipun perilaku yang dijanjikan tidak berubah. Kasus uji yang baik memeriksa nilai balik, kode kegagalan, dan efek samping yang terdokumentasi, sehingga implementasi boleh berubah tanpa menggagalkan pengujian yang sah.
Kesalahan kedua adalah asersi yang selalu lulus. Pemeriksaan berbentuk assert(nilai) pada table memang selalu berhasil karena table apa pun bernilai benar, sehingga kasus yang seharusnya membandingkan isi table malah tidak memeriksa apa pun. Asersi lokal asersi_sama pada kerangka ini ada justru untuk menghindari jebakan tersebut, dan setiap asersi harus menyatakan nilai harapan yang konkret.
Kesalahan ketiga adalah kasus uji yang berjalan tanpa pelindung. Satu kasus yang mengangkat error di luar pcall menghentikan seluruh berkas pengujian, sehingga kasus setelahnya tidak pernah dijalankan dan jumlah kasus yang benar-benar diuji berkurang diam-diam. Fungsi uji pada kerangka ini membungkus setiap kasus dengan pcall untuk alasan tersebut, dan kasus baru selalu ditambahkan melalui fungsi itu, bukan dengan pemanggilan langsung.
Ringkasan#
- Kerangka uji proyek dibuat sendiri dalam satu berkas Lua polos, karena proyek tersemat tidak boleh bergantung pada pustaka pengujian eksternal.
- Fungsi
ujimembungkus setiap kasus denganpcall, asersi lokal membandingkan nilai dan table per kunci dengan error level 0, dankode_pertamamenguji kode kegagalan dari daftar pelanggaran terurut. - Enam kasus uji mengunci kontrak mesin, yaitu kandidat sah diterima, pelanggaran dilaporkan dengan kodenya, nilai bawaan digunakan, dan error terstruktur dikembalikan oleh
muat_aman. os.exitmenyatakan keberhasilan pengujian melalui kode keluar, sehingga pengujian dapat dijalankan oleh alat otomatis.
Referensi#
- Manual Referensi Lua 5.4 — Lua.org
— Bagian 6.1 tentang fungsi dasar, termasuk
assertdanos.exit, menjadi rujukan asersi dan kode keluar pengujian. - Programming in Lua, edisi keempat — Lua.org
— Buku rujukan penggunaan
pcallsebagai dasar pembungkus pelaksanaan terlindungi pada kerangka uji.