Sepanjang Course ini kita menjadikan kompilator Rust sebagai rekan kerja terdekat: galat tipe, pelanggaran aturan ownership, dan reference yang menggantung selalu tertangkap sebelum program sempat berjalan. Namun kompilator punya batas yang perlu kita akui secara jujur. Kompilator memeriksa apakah kode sah menurut aturan bahasa, bukan apakah kode benar menurut maksud bisnis kita. Fungsi perhitungan diskon dengan rumus yang salah tetap lolos kompilasi tanpa satu pun peringatan karena bentuknya rapi: parameter masuk bertipe u64, nilai keluar juga u64. Section Testing ini membahas lapisan jaminan kedua: test yang kita tulis sendiri, dijalankan oleh cargo, dan bertugas memastikan perilaku kode sesuai harapan. Kita mulai dari filosofinya, yaitu pembagian peran yang tegas antara kompilator dan test.

Dua Lapis Jaminan Kualitas #

Kualitas perangkat lunak di Rust dijaga oleh dua mekanisme yang bekerja pada wilayah berbeda. Lapisan pertama adalah kompilator, yang bekerja sebelum program berjalan dan berfokus pada bentuk kode: tipe yang cocok, kepemilikan yang jelas, peminjaman yang taat aturan. Lapisan kedua adalah test, yang bekerja saat program sudah berjalan dan berfokus pada perilaku kode: apakah hasil perhitungan benar, apakah input tidak valid ditolak, apakah urutan langkah bisnis sesuai.

Pembagian wilayah ini bisa kita petakan seperti tabel berikut.

Sumber galatPenangkap utama
Tipe argumen tidak cocok dengan parameterKompilator
Variabel dipakai setelah nilainya di-moveKompilator
Dua mutable reference aktif pada data yang samaKompilator
Rumus diskon menghasilkan angka yang salahTest
Input di luar batas wajar tidak ditolakTest
Urutan langkah proses bisnis terbalikTest

Baris-baris teratas pada tabel di atas sudah kita alami sendiri di Section Ownership dan Section Struktur Data. Baris-baris bawahnya adalah wilayah yang sama sekali gelap bagi kompilator: kompilator tidak tahu bahwa diskon 20 persen dari harga Rp100.000 seharusnya menghasilkan potongan Rp20.000. Pengetahuan semacam itu hanya hidup di kepala kita sebagai penulis kode, dan satu-satunya cara memindahkannya ke dalam proyek adalah menuliskannya sebagai test.

Mekanisme Kerja cargo test #

Perintah cargo test bukan sekadar pengganti cargo run. Saat perintah tersebut dijalankan, cargo melakukan serangkaian langkah yang berbeda dari proses build biasa.

Pertama, cargo mengompilasi crate kita dengan konfigurasi khusus: flag cfg(test) diaktifkan, sehingga seluruh kode yang ditandai #[cfg(test)] ikut dikompilasi. Kedua, cargo membangun sebuah test harness — program runner bawaan yang memiliki fungsi main sendiri — lalu memasukkan setiap fungsi bertanda #[test] ke dalam daftar test harness tersebut. Ketiga, harness menjalankan test satu per satu: test yang selesai tanpa panic dinyatakan lulus (ok), sedangkan test yang mengalami panic dinyatakan gagal (FAILED). Keempat, cargo mengumpulkan laporan dari semua test ke dalam ringkasan test result yang mencatat jumlah lulus, gagal, diabaikan, dan difilter.

Diagram di atas menegaskan posisi panic sebagai mekanisme kegagalan resmi test Rust. Perhatikan bahwa seluruh jalur keputusan harness bergantung pada satu pertanyaan saja: apakah fungsi test tersebut panic. Konsekuensinya, assertion seperti assert_eq! yang kita jumpai pada Chapter-Chapter berikutnya hanyalah cara terstruktur untuk memicu panic ketika harapan kita dilanggar.

Kasus Nyata: Bug yang Lolos dari Kompilator #

Kita buktikan batas kompilator dengan kasus nyata. Buat proyek baru dengan cargo new toko --lib, lalu isi src/lib.rs dengan fungsi yang menghitung harga setelah diskon. Versi pertama kita sengaja memuat bug: rumusnya menghitung besar potongan, bukan harga akhir.

rust
pub fn harga_setelah_diskon(harga: u64, persen: u64) -> u64 {
    harga * persen / 100
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn diskon_dua_puluh_persen() {
        assert_eq!(harga_setelah_diskon(100_000, 20), 80_000);
    }
}

Sebelum membicarakan test, periksa dulu pendapat kompilator dengan cargo build:

bash
cargo build

Output yang Dihasilkan:

text
   Compiling toko v0.1.0 (/tmp/w4e-filosofi-testing-rust/toko)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06s

Kompilator tidak mengeluarkan satu pun peringatan: operasi u64 * u64 / 100 menghasilkan u64, tipe nilai kembalian cocok, dan aturan ownership tidak tersentuh. Alamat proyek pada baris Compiling mengikuti lokasi folder di komputer masing-masing — di komputer penulis proyek berada di /tmp/w4e-filosofi-testing-rust/toko. Sekarang jalankan test:

bash
cargo test

Output yang Dihasilkan:

text
   Compiling toko v0.1.0 (/tmp/w4e-filosofi-testing-rust/toko)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.09s
     Running unittests src/lib.rs (target/debug/deps/toko-437cac73a95d3df8)

running 1 test
test tests::diskon_dua_puluh_persen ... FAILED

failures:

---- tests::diskon_dua_puluh_persen stdout ----

thread 'tests::diskon_dua_puluh_persen' (14754) panicked at src/lib.rs:11:9:
assertion `left == right` failed
  left: 20000
 right: 80000
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::diskon_dua_puluh_persen

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

error: test failed, to rerun pass `--lib`

Mari kita bedah bagian-bagian penting dari kode dan laporan di atas:

  1. Baris 5 (#[cfg(test)]): attribute yang menyatakan modul tests hanya dikompilasi saat pengujian. cargo build di atas tidak menyertakan modul ini, sedangkan cargo test menyertakannya.
  2. Baris 6 (mod tests): modul penampung seluruh test, mengikuti konvensi komunitas Rust. Nama test pada laporan tampil sebagai tests::diskon_dua_puluh_persen karena modul ini ikut tercetak.
  3. Baris 7 (use super::*;): membawa seluruh item publik dari modul induk ke dalam scope modul tests, sehingga harga_setelah_diskon bisa dipanggil tanpa prefiks.
  4. Baris 8 (#[test]): menandai fungsi di bawahnya sebagai satu unit test; test harness akan menjalankan fungsi ini.
  5. Baris 11 (assert_eq!(harga_setelah_diskon(100_000, 20), 80_000)): assertion yang membandingkan hasil fungsi dengan nilai harapan. Baris inilah yang memicu panic pada src/lib.rs:11:9.
  6. Baris left: 20000 / right: 80000 (pada laporan): sisi left adalah argumen pertama assert_eq! — hasil aktual fungsi (potongan Rp20.000) — sedangkan sisi right adalah argumen kedua — harga akhir yang kita harapkan (Rp80.000). Ketimpangan dua angka ini adalah bukti pertama bahwa rumus fungsi salah.

Laporan di atas juga memperlihatkan struktur kegagalan yang khas: bagian failures: pertama membuka rincian, bagian ---- tests::... stdout ---- menampilkan pesan panic lengkap beserta lokasinya, dan bagian failures: kedua merangkum nama test yang gagal. Baris error: test failed, to rerun pass --lib memberi tahu cara mengulang hanya suite unit test tanpa menjalankan suite lain.

Memperbaiki hingga Lulus #

Test yang gagal bukan kabar buruk; test yang gagal adalah test yang bekerja. Kini kita tahu persis lokasi kekeliruan: fungsi mengembalikan potongan, bukan harga akhir. Perbaikannya adalah mengurangi potongan dari harga awal:

rust
pub fn harga_setelah_diskon(harga: u64, persen: u64) -> u64 {
    harga - harga * persen / 100
}

Jalankan cargo test sekali lagi.

Output yang Dihasilkan:

text
   Compiling toko v0.1.0 (/tmp/w4e-filosofi-testing-rust/toko)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.07s
     Running unittests src/lib.rs (target/debug/deps/toko-437cac73a95d3df8)

running 1 test
test tests::diskon_dua_puluh_persen ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests toko

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Siklus merah-hijau yang barusan kita jalankan — tulis test, lihat gagal, perbaiki kode, lihat lulus — adalah pola kerja paling dasar dalam pengujian. Baris 1 passed kini hadir sebagai bukti tertulis bahwa harga setelah diskon 20 persen dari Rp100.000 memang Rp80.000, dan bukti tersebut bisa diperiksa ulang kapan saja oleh siapa saja yang menjalankan cargo test. Bagian Doc-tests toko pada akhir laporan muncul karena cargo selalu turut memeriksa contoh kode di doc comment; crate kita belum memilikinya, maka tertulis running 0 tests — topik yang kita tutup di Chapter terakhir section ini.

Ringkasan #

Kompilator Rust menjamin kode kita sah secara bahasa: tipe, ownership, dan borrowing diperiksa hingga tuntas. Test menjamin kode kita benar secara perilaku: angka, batas input, dan urutan langkah bisnis diverifikasi lewat cargo test. Perintah cargo test mengompilasi crate dengan cfg(test) aktif, membangun test harness, menjalankan tiap fungsi #[test], dan melaporkan ok atau FAILED berdasarkan ada tidaknya panic. Bug rumus diskon di Chapter ini lolos dari cargo build namun tertangkap oleh satu assertion. Chapter berikutnya membedah rumah test itu sendiri: modul tests, attribute #[test], dan peran #[cfg(test)].