Ekspektasi

Memakai expectEqual, asersi error, dan verifikasi allocator dalam test.

Daftar isi

Blok test baru bermanfaat penuh apabila isinya memeriksa perilaku kode secara ketat. Topik ini membahas fungsi ekspektasi yang disediakan std.testing, mulai dari perbandingan nilai, teks, dan deret, asersi error, hingga verifikasi allocator di dalam test. Contohnya diterapkan pada berkas statistik.zig yang memuat fungsi statistik sederhana, dan pola pesan yang muncul ketika ekspektasi tidak terpenuhi dijelaskan agar kegagalan test dapat dibaca dengan cepat.

Konsep#

Fungsi ekspektasi pada std.testing#

Seluruh pemeriksaan test pada pustaka standar berada di dalam namespace std.testing. Fungsi ekspektasinya mengembalikan error apabila pemeriksaan tidak terpenuhi, sehingga setiap pemanggilan diawali try agar error tersebut naik ke test runner. Test runner menangkap error itu, menandai test yang bersangkutan gagal, lalu melanjutkan ke test berikutnya. Karena itu, satu blok test boleh memuat banyak asersi (assertion) tanpa saling mengganggu.

FungsiPemeriksaan yang dilakukan
std.testing.expectEkspresi boolean harus bernilai benar
std.testing.expectEqualNilai aktual sama dengan nilai harapan
std.testing.expectEqualStringsDua string sama isinya
std.testing.expectEqualSlicesDua slice sama panjang dan sama isinya
std.testing.expectErrorEkspresi gagal dengan error yang ditentukan

Pemilihan fungsi mengikuti jenis nilai yang diperiksa. Fungsi expect cocok untuk sifat yang dinyatakan sebagai ekspresi boolean, sedangkan expectEqual lebih diutamakan untuk perbandingan nilai karena harapan dan aktual dicantumkan terpisah pada laporan kegagalan. Fungsi expectEqualStrings dan expectEqualSlices menyandingkan isi string dan deret secara utuh, sehingga perbedaan posisinya langsung terlihat tanpa pemeriksaan satu per satu.

Tipe nilai harapan yang eksplisit#

Fungsi expectEqual menerima dua nilai dengan tipe yang sama, dan tipe harapan mengikuti parameter pertamanya. Literal bilangan pada Zig tanpa anotasi bertipe comptime_int atau comptime_float, yaitu tipe waktu kompilasi yang belum memiliki lebar konkret, sehingga literal itu perlu diikat ke tipe target dengan @as sebelum dibandingkan. Pola yang paling umum adalah expectEqual(@as(u32, 2), hasil), yang menyatakan harapan 2 bertipe u32 secara eksplisit.

Apabila tipe kedua nilai tidak cocok, kekeliruan itu terdeteksi saat kompilasi, bukan saat test berjalan. Pesan kesalahannya menunjuk baris pembanding, sehingga perbaikannya cepat dilakukan. Alternatif penulisan lain adalah menandai variabel harapan dengan tipe konkret, misalnya const harapan: u32 = 2, kemudian meneruskannya ke expectEqual. Apa pun bentuknya, prinsipnya sama, yaitu tipe harapan dan tipe aktual harus sama persis.

zig Contoh: harapan dengan tipe eksplisit
// tidak-dijalankan
test "pembulatan ke bawah pembagian bulat" {
    try std.testing.expectEqual(@as(u32, 2), 10 / 4);
}

Contoh Lengkap#

Menerapkan ekspektasi pada berkas statistik#

Berkas statistik.zig berikut mengumpulkan pemakaian fungsi ekspektasi pada satu kasus yang utuh. Fungsi rataRata menghitung rata-rata deretan bilangan pecahan, fungsi maksimum mencari nilai terbesar sekaligus menolak deretan kosong dengan error, dan fungsi salin menyalin deretan ke memori baru melalui allocator.

zig statistik.zig
// tidak-dijalankan
const std = @import("std");

// menghitung rata-rata aritmetika dari deretan nilai
pub fn rataRata(nilai: []const f64) f64 {
    var jumlah: f64 = 0;
    for (nilai) |n| {
        jumlah += n;
    }
    return jumlah / @as(f64, @floatFromInt(nilai.len));
}

// mencari nilai terbesar, gagal dengan error bila deretan kosong
pub fn maksimum(nilai: []const u32) error{DeretanKosong}!u32 {
    if (nilai.len == 0) return error.DeretanKosong;
    var terbesar = nilai[0];
    for (nilai[1..]) |n| {
        if (n > terbesar) terbesar = n;
    }
    return terbesar;
}

// menyalin deretan ke memori baru yang dialokasikan
pub fn salin(allocator: std.mem.Allocator, nilai: []const u32) ![]u32 {
    const hasil = try allocator.alloc(u32, nilai.len);
    @memcpy(hasil, nilai);
    return hasil;
}

test "rata-rata aritmetika sederhana" {
    const nilai = [_]f64{ 6, 7, 8, 9 };
    try std.testing.expectEqual(@as(f64, 7.5), rataRata(&nilai));
}

test "rata-rata berada di dalam rentang nilai" {
    const nilai = [_]f64{ 4, 10, 6 };
    const r = rataRata(&nilai);
    try std.testing.expect(r >= 4 and r <= 10);
}

test "maksimum deretan nilai" {
    const nilai = [_]u32{ 42, 17, 88, 5 };
    try std.testing.expectEqual(@as(u32, 88), maksimum(&nilai));
}

test "maksimum deretan kosong menghasilkan error" {
    try std.testing.expectError(error.DeretanKosong, maksimum(&[_]u32{}));
}

test "salin menghasilkan urutan yang sama" {
    const sumber = [_]u32{ 10, 20, 30 };
    const hasil = try salin(std.testing.allocator, &sumber);
    defer std.testing.allocator.free(hasil);
    try std.testing.expectEqualSlices(u32, &sumber, hasil);
}

test "ringkasan rata-rata sebagai teks" {
    var buffer: [32]u8 = undefined;
    const nilai = [_]f64{ 8, 9, 10 };
    const teks = try std.fmt.bufPrint(&buffer, "rata-rata = {d:.1}", .{rataRata(&nilai)});
    try std.testing.expectEqualStrings("rata-rata = 9.0", teks);
}

Enam test di dalamnya menggambarkan pemilihan asersi sesuai kebutuhan. Test pertama membandingkan hasil pecahan dengan expectEqual bertipe f64, sedangkan test kedua memakai expect karena yang diperiksa adalah sifat rentang, bukan nilai pasti. Test kelima menyalin deretan melalui std.testing.allocator lalu membandingkan hasilnya dengan expectEqualSlices, dan test keenam memformat teks ringkas ke dalam buffer sebelum disandingkan dengan expectEqualStrings.

Jalankan sekumpulan test tersebut dengan perintah berikut.

bash
# menguji seluruh fungsi statistik
zig test statistik.zig
text Keluaran
1/6 statistik.test.rata-rata aritmetika sederhana...OK
2/6 statistik.test.rata-rata berada di dalam rentang nilai...OK
3/6 statistik.test.maksimum deretan nilai...OK
4/6 statistik.test.maksimum deretan kosong menghasilkan error...OK
5/6 statistik.test.salin menghasilkan urutan yang sama...OK
6/6 statistik.test.ringkasan rata-rata sebagai teks...OK
All 6 tests passed.

Seluruh test lolos, dan baris pelaporan kemajuannya menampilkan keenam nama test dengan urutan deklarasinya. Susunan seperti ini, yaitu fungsi pendek dengan test yang menutupinya, merupakan pola yang digunakan di sepanjang jalur ini. Perubahan perilaku pada salah satu fungsi akan langsung terlihat pada nama test yang gagal.

Asersi error dengan expectError#

Fungsi yang memodelkan kegagalan dengan error union dapat diuji dengan std.testing.expectError. Fungsi ini menerima error yang diharapkan pada parameter pertama dan ekspresi yang dapat gagal pada parameter kedua, kemudian dinyatakan lolos apabila ekspresi tersebut memang gagal dengan error yang sama persis. Apabila ekspresi berhasil tanpa error, atau gagal dengan error lain, asersinya menandai test sebagai gagal.

Pada berkas statistik, fungsi maksimum mengembalikan error.DeretanKosong apabila deretan masukannya kosong. Test maksimum deretan kosong menghasilkan error memanggil expectError(error.DeretanKosong, maksimum(&[_]u32{})) untuk memastikan penolakan itu benar-benar terjadi. Pemeriksaan searah ini melengkapi test jalur normal, sehingga kedua kemungkinan keluar fungsi tercakup. Kebiasaan yang baik adalah menyediakan satu test untuk setiap jenis error yang dinyatakan oleh fungsi.

Verifikasi allocator dalam test#

Allocator yang dipakai test dapat diambil dari std.testing.allocator, yaitu allocator pustaka standar yang mencatat seluruh alokasi dan memeriksa pembebasonya pada akhir proses. Test pada berkas statistik menggunakannya untuk menguji fungsi salin, dengan pola yang lazim, yaitu membebaskan memori melalui defer agar pembebasan tetap terjadi ketika asersi di tengah test gagal.

Manfaat terbesar dari allocator ini adalah deteksi kebocoran (leak) yang berjalan otomatis. Apabila sebuah test mengalokasikan memori tanpa membebaskannya, test itu tetap tercatat lolos pada baris pelaporan kemajuannya, tetapi proses test berakhir dengan kode keluar 1. Pustaka standar mencetak laporan yang menyebut alamat memori hasil alokasi beserta lokasi alokasinya, ditutup baris 1 tests leaked memory. Alamat dan jalur pada laporan berubah antar eksekusi, sehingga keluarannya tidak dikutip persis di sini dan cukup pola pesannya yang dijelaskan.

Pola pesan kegagalan#

Ekspektasi yang tidak terpenuhi menghasilkan pesan yang terstruktur, dan membacanya dengan cepat berarti mengenali bagiannya. Contoh berikut memanggil expectEqual dengan harapan yang sengaja salah, yaitu 3, padahal pemanggilan bagi(10, 4) menghasilkan 2 karena pembagian bilangan bulat membulatkan ke bawah.

zig Contoh: ekspektasi yang tidak terpenuhi
// tidak-dijalankan
fn bagi(a: u32, b: u32) u32 {
    return a / b;
}

test "pembagian menghasilkan tiga" {
    try std.testing.expectEqual(@as(u32, 3), bagi(10, 4));
}

Baris pelaporan kemajuan test tersebut berakhir dengan keterangan expected 3, found 2, yaitu pasangan nilai harapan dan nilai aktual, lalu diikuti label FAIL (TestExpectedEqual). Pesan selanjutnya menunjuk berkas dan baris asersi yang gagal, kemudian diakhiri rekap berupa 0 passed; 0 skipped; 1 failed. beserta kode keluar 1. Bagian jejak tumpukan (stack trace) pada pesan memuat alamat memori dan jalur pustaka yang berubah antar eksekusi, sehingga bagian itu tidak dikutip persis di sini. Bagian yang stabil, yaitu nama test, pasangan harapan dan aktual, serta lokasi asersinya, sudah cukup untuk menemukan penyebabnya.

Kesalahan umum#

Kesalahan pertama berupa mengoperasikan nilai bertipe !T langsung pada fungsi ekspektasi. Fungsi seperti expectEqual membandingkan nilai yang sudah berhasil, bukan error union, sehingga pemanggilannya atas hasil fungsi yang dapat gagal ditolak kompilator. Hasilnya dibuka lebih dahulu dengan try, dan errornya diuji dengan expectError, bukan dengan expectEqual.

Kesalahan kedua berupa menyamakan expectEqual dan expectEqualStrings pada data teks. Dua slice []const u8 dengan isi sama tidak dijamin identik sebagai pointer, sehingga expectEqual atas dua slice teks dapat gagal meskipun isinya sama. Perbandingan isi teks memakai expectEqualStrings, dan pesannya khusus menampilkan kedua teks tersebut pada saat gagal.

Ringkasan#

  • Fungsi ekspektasi std.testing mengembalikan error ketika pemeriksaan gagal, sehingga pemanggilannya diawali try.
  • expectEqual membandingkan dua nilai dengan tipe yang sama persis, dan tipe harapannya dinyatakan eksplisit melalui @as.
  • expectError memastikan ekspresi gagal dengan error tertentu, sedangkan std.testing.allocator memeriksa pembebasan memori dan melaporkan kebocoran pada akhir proses.
  • Pesan kegagalan menyebut nama test, pasangan harapan dan aktual, serta lokasi asersinya, dan proses test berakhir dengan kode keluar 1.

Referensi#