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 galat | Penangkap utama |
|---|---|
| Tipe argumen tidak cocok dengan parameter | Kompilator |
| Variabel dipakai setelah nilainya di-move | Kompilator |
| Dua mutable reference aktif pada data yang sama | Kompilator |
| Rumus diskon menghasilkan angka yang salah | Test |
| Input di luar batas wajar tidak ditolak | Test |
| Urutan langkah proses bisnis terbalik | Test |
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.
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:
cargo buildOutput yang Dihasilkan:
Compiling toko v0.1.0 (/tmp/w4e-filosofi-testing-rust/toko)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.06sKompilator 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:
cargo testOutput yang Dihasilkan:
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:
- Baris 5 (
#[cfg(test)]): attribute yang menyatakan modultestshanya dikompilasi saat pengujian.cargo builddi atas tidak menyertakan modul ini, sedangkancargo testmenyertakannya. - Baris 6 (
mod tests): modul penampung seluruh test, mengikuti konvensi komunitas Rust. Nama test pada laporan tampil sebagaitests::diskon_dua_puluh_persenkarena modul ini ikut tercetak. - Baris 7 (
use super::*;): membawa seluruh item publik dari modul induk ke dalam scope modultests, sehinggaharga_setelah_diskonbisa dipanggil tanpa prefiks. - Baris 8 (
#[test]): menandai fungsi di bawahnya sebagai satu unit test; test harness akan menjalankan fungsi ini. - Baris 11 (
assert_eq!(harga_setelah_diskon(100_000, 20), 80_000)): assertion yang membandingkan hasil fungsi dengan nilai harapan. Baris inilah yang memicupanicpadasrc/lib.rs:11:9. - Baris
left: 20000 / right: 80000(pada laporan): sisileftadalah argumen pertamaassert_eq!— hasil aktual fungsi (potongan Rp20.000) — sedangkan sisirightadalah 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:
pub fn harga_setelah_diskon(harga: u64, persen: u64) -> u64 {
harga - harga * persen / 100
}Jalankan cargo test sekali lagi.
Output yang Dihasilkan:
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.00sSiklus 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)].