Test dalam build.zig

Menyambungkan test suite ke graf build sehingga zig build test menjalankannya.

Daftar isi

Test yang dijalankan langsung dengan zig test menuntut perintah manual pada setiap berkas, dan kebiasaan itu mudah terlewat ketika proyek bertumbuh. Topik ini membangun proyek mini yang terdiri atas modul pustaka, program utama, dan build.zig yang menyambungkan kumpulan test ke langkah test. Setelah topik ini dibaca, seluruh test proyek dapat dijalankan melalui zig build test sebagai bagian dari graf build (build graph), yaitu jaringan langkah yang dieksekusi oleh pengelola build Zig.

Konsep#

Struktur proyek mini#

Proyek mini pada topik ini memisahkan pustaka dari program pemakainya, dan test menempel pada pustaka. Struktur berkasnya terdiri atas tiga bagian, yaitu modul pustaka statistik.zig, program utama main.zig, dan berkas build build.zig, seluruhnya berdampingan pada satu direktori proyek.

text
proyek-statistik/
├── build.zig
├── main.zig
└── statistik.zig

Pemisahan peran ini mengikuti kebiasaan yang dianjurkan pada proyek Zig, yaitu logika inti berada pada modul pustaka yang mudah diuji, sedangkan main.zig hanya berisi penghubung yang tipis. Test ditulis pada berkas yang sama dengan kode yang diujinya, sehingga zig test terhadap berkas modul langsung menemukannya. Berkas build kemudian mengambil test tersebut dan menjadikannya langkah resmi proyek.

build.zig yang menyambungkan test#

Berkas build.zig berikut mendeklarasikan modul pustaka, program utama, dan langkah test dalam satu graf.

zig build.zig
// tidak-dijalankan: berkas build dieksekusi lewat perintah zig build
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});

    // modul pustaka statistik beserta test suite-nya
    const statistik_mod = b.addModule("statistik", .{
        .root_source_file = b.path("statistik.zig"),
        .target = target,
    });

    const statistik_tests = b.addTest(.{
        .root_module = statistik_mod,
    });
    const run_statistik_tests = b.addRunArtifact(statistik_tests);

    // program utama yang mengimpor modul statistik
    const exe_mod = b.createModule(.{
        .root_source_file = b.path("main.zig"),
        .target = target,
    });
    exe_mod.addImport("statistik", statistik_mod);

    const exe = b.addExecutable(.{
        .name = "rekap-nilai",
        .root_module = exe_mod,
    });
    b.installArtifact(exe);

    // langkah test menyambungkan kumpulan test ke graf build
    const test_step = b.step("test", "Run tests");
    test_step.dependOn(&run_statistik_tests.step);
}

Fungsi build merupakan titik masuk graf build. Panggilan b.addModule("statistik", ...) mendeklarasikan modul bernama statistik dengan akar berkas statistik.zig, dan panggilan b.addTest yang menerima modul itu menghasilkan artefak test, yaitu program khusus yang menjalankan seluruh blok test pada modul tersebut. Artefak test belum berarti apa-apa sebelum dieksekusi, sehingga b.addRunArtifact membuat langkah yang menjalankannya.

Sambungan terakhir dilakukan oleh b.step("test", "Run tests"), yang membuat langkah bernama test, kemudian test_step.dependOn menjadikan pelaksanaan test sebagai kebergantungan langkah itu. Perintah zig build test memilih langkah bernama tersebut dan mengeksekusi seluruh kebergantungannya. Selain itu, b.installArtifact(exe) mendaftarkan program utama pada langkah bawaan install, sehingga zig build tanpa argumen tetap menghasilkan program yang dapat dijalankan.

Contoh Lengkap#

Modul pustaka dan program utamanya#

Berkas modul berisi dua fungsi statistik beserta tiga test yang menutupinya.

zig statistik.zig
// tidak-dijalankan: berkas pengujian dieksekusi lewat perintah zig build test
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;
}

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

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{}));
}

Berkas program utama mengimpor modul dengan nama statistik yang telah dideklarasikan pada build.zig.

zig main.zig
// tidak-dijalankan: modul statistik tersambung melalui build.zig
const std = @import("std");
const statistik = @import("statistik");

pub fn main() !void {
    const nilai = [_]u32{ 72, 85, 90, 68 };
    const terbesar = try statistik.maksimum(&nilai);
    std.debug.print("Nilai terbesar: {d}\n", .{terbesar});
}

Berkas statistik.zig memuat fungsi publik yang disertai test pada berkas yang sama, dan testnya tidak lagi dijalankan manual karena berkas build telah mengambilnya ke dalam graf. Berkas main.zig memperlihatkan sisi pemakai, yaitu @import("statistik") yang menemukan modul berdasarkan nama yang diberikan kepada addImport pada build.zig. Pemanggilan statistik.maksimum di dalam main menegaskan bahwa program benar-benar bergantung pada modul pustakanya.

Perlu diperhatikan bahwa @import("statistik") pada berkas program hanya valid karena build.zig memasangkan modul itu ke modul program melalui exe_mod.addImport. Nama modul menjadi nama import, sehingga keduanya harus konsisten. Dengan pemisahan ini, logika statistik dapat diuji secara terpisah dari antarmuka programnya.

Menjalankan test melalui graf build#

Seluruh langkah kini dapat dijalankan dari akar proyek. Perintah pertama membangun program utama, perintah kedua menjalankannya, dan perintah ketiga mengeksekusi kumpulan test melalui langkah test.

bash
# membangun program utama melalui langkah bawaan install
zig build
# menjalankan program rekap nilai yang dihasilkan
./zig-out/bin/rekap-nilai
# menjalankan seluruh test melalui langkah test
zig build test
# menampilkan kode keluar dari langkah test
echo "Kode keluar: $?"
text Keluaran
Nilai terbesar: 90
Kode keluar: 0

Perhatikan bahwa zig build test tidak mencetak apa pun pada keberhasilan. Program test berkomunikasi dengan pengelola build melalui saluran internal, sehingga keberhasilan ditandai oleh kode keluar 0 dan perintah echo terakhir pada blok tersebut berperan memperlihatkannya. Perilaku ini berbeda dari zig test langsung yang mencetak All N tests passed., karena pada mode build pelaporan test dikelola oleh pengelola build, bukan oleh terminal.

Ketika salah satu test gagal, pengelola build yang mengambil alih pelaporannya. Keluarannya memuat nama test yang gagal, lokasi asersinya, serta ringkasan langkah yang berakhir gagal, ditutup dengan kode keluar 1. Alamat memori dan jalur cache pada laporan berubah antar eksekusi, sehingga bagian tersebut tidak dikutip persis di sini.

Test sebagai bagian dari graf build#

Setelah tersambung, test bukan lagi tindakan yang terpisah dari build, melainkan langkah yang memiliki kebergantungan tersendiri. Langkah pelaksana test bergantung pada langkah kompilasi test, dan langkah kompilasi test bergantung pada berkas sumber modul. Karena itulah perubahan sekecil apa pun pada statistik.zig membuat test dikompilasi dan dijalankan ulang, sedangkan keadaan tanpa perubahan memanfaatkan cache build.

Pengelola build mengeksekusi langkah-langkah yang saling tidak bergantung secara paralel, dan test ikut dalam skema ini. Pada proyek yang memuat beberapa modul, test modul-modul itu berjalan berdampingan selama kebergantungannya tidak saling menunggu. Kemampuan ini juga menjadi dasar otomatisasi, misalnya satu pemanggilan zig build test pada alat integrasi berkelanjutan sudah mencakup seluruh test yang tersambung.

Kesalahan umum#

Kesalahan pertama berupa menulis test step tanpa menghubungkannya ke langkah tingkat atas. b.addTest hanya membuat langkah pengujian pada graf, dan tanpa b.step("test", ...).dependOn(...) perintah zig build test tidak menjalankan apa pun. Gejalanya berupa build yang lulus tanpa satu pun test dieksekusi, dan pemeriksaannya dimulai dari dependensi test step.

Kesalahan kedua berupa menautkan modul pustaka tanpa target yang diketahui. Modul yang dibuat untuk diuji wajib membawa field target, dan kelapatannya berupa kepanikan build runner yang menyebut target tidak diketahui. Menyalin target dari standardTargetOptions ke dalam modul menutup kegagalan ini, dan pola yang sama berlaku pada modul program utamanya.

Ringkasan#

  • b.addTest menghasilkan artefak test dari sebuah modul, dan b.addRunArtifact menjadikan pelaksanaannya sebuah langkah.
  • Langkah bernama test dibuat dengan b.step, dan dependOn menyambungkan pelaksanaan test ke langkah itu sehingga zig build test menjalankannya.
  • Pada keberhasilan, zig build test tidak mencetak keluaran dan menandai hasilnya melalui kode keluar 0.
  • Test yang tersambung menjadi bagian graf build, sehingga dijalankan ulang saat sumbernya berubah dan berjalan paralel dengan langkah lain yang tidak bergantung.

Referensi#