Chapter sebelumnya memperlihatkan satu test yang menangkap bug rumus diskon. Sekarang kita masuk ke rumah tempat test itu tinggal: modul tests yang ditandai #[cfg(test)] dan fungsi-fungsi ber-atribut #[test] di dalamnya. Rust tidak mengharuskan kita membuat proyek terpisah, direktori khusus, atau file baru untuk unit test. Konvensi komunitas justru sebaliknya: test ditempatkan menempel pada kode yang diuji, di file yang sama, biasanya di bagian paling bawah. Penempatan seperti ini bukan gaya-gayaan — kedekatan posisi memudahkan kita membaca kode produksi dan ekspektasinya berdampingan, serta memberi test akses ke detail internal crate. Chapter ini membedah anatomi mod tests selengkapnya, cara menjalankan sebagian test dengan filter nama, dan alasan test pribadi boleh menguji fungsi yang bahkan tidak ber-pub.
Menyiapkan Proyek Kasir #
Buat proyek crate pustaka baru dengan nama kasir:
cargo new kasir --libLalu isi src/lib.rs dengan fungsi penghitung diskon beserta tiga unit test untuknya:
pub fn hitung_diskon(harga: u64, persen: u64) -> u64 {
harga * persen / 100
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diskon_persen_penuh() {
assert_eq!(hitung_diskon(100_000, 20), 20_000);
}
#[test]
fn diskon_nol_persen() {
assert_eq!(hitung_diskon(250_000, 0), 0);
}
#[test]
fn diskon_seratus_persen() {
assert_eq!(hitung_diskon(80_000, 100), 80_000);
}
}Jalankan seluruh test di dalam direktori proyek:
cargo testOutput yang Dihasilkan:
Compiling kasir v0.1.0 (/tmp/w4e-unit-test-mod-tests/kasir)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.14s
Running unittests src/lib.rs (target/debug/deps/kasir-120bc9ec7040e8c8)
running 3 tests
test tests::diskon_nol_persen ... ok
test tests::diskon_persen_penuh ... ok
test tests::diskon_seratus_persen ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests kasir
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sOutput di atas adalah hasil eksekusi nyata di komputer penulis; durasi dan hash binary akan berbeda di mesin lain. Mari kita bedah kode baris-per-baris:
- Baris 1 (
pub fn hitung_diskon(harga: u64, persen: u64) -> u64): fungsi produksi yang menjadi objek uji. Fungsi ini menghitung potongan harga dengan pembulatan pembagian bilangan bulat. - Baris 4 (
#[cfg(test)]): attribute konfigurasi kompilasi bersyarat. Modultestshanya dikompilasi saat kita menjalankancargo test; perintahcargo builduntuk konsumen crate mengabaikan modul ini sepenuhnya, sehingga kode test tidak pernah ikut terbawa ke hasil build produksi. - Baris 5 (
mod tests): modul penampung test dengan nama konvensitests. Seluruh fungsi test di dalamnya dilaporkan dengan prefikstests::, terlihat jelas pada baristest tests::diskon_nol_persen ... ok. - Baris 6 (
use super::*;): glob import dari modul induk. Modultestsadalah anak dari modul crate root, jadi semua item disrc/lib.rs— termasukhitung_diskon— masuk ke scope test tanpa prefiks. - Baris 8 dan 15 (
#[test]): attribute yang mendaftarkan fungsi tepat di bawahnya ke test harness. Tanpa attribute ini, fungsi biasa tidak akan pernah dijalankan olehcargo test. - Baris 10, 17, dan 22 (tiga pemanggilan
assert_eq!): tiga test menguji tiga kondisi batas yang berbeda — persen wajar (20), persen terendah (0), dan persen tertinggi (100). Memilih nilai batas seperti ini adalah kebiasaan yang baik karena kesalahan logika paling sering muncul di tepi rentang.
Baris ringkasan test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out layak dibaca satu per satu: 3 passed dan 0 failed mencatat hasil test, 0 ignored berarti tidak ada test bertanda #[ignore] yang dilewati, 0 measured berarti tidak ada benchmark yang diukur, dan 0 filtered out berarti tidak ada test yang tersaring karena kita tidak memakai filter. Bagian Doc-tests kasir muncul karena crate belum memiliki doc test — topik Chapter terakhir section ini.
Menyaring Test dengan Filter Nama #
Saat satu test gagal dan kita ingin mengulangnya tanpa menunggu test lain, filter nama adalah alatnya. Cukup tambahkan sepotong nama test setelah cargo test:
cargo test diskon_nol_persenOutput yang Dihasilkan:
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/kasir-120bc9ec7040e8c8)
running 1 test
test tests::diskon_nol_persen ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00sPerhatikan dua perubahan pada laporan. Pertama, hanya tests::diskon_nol_persen yang dijalankan; kata yang kita ketik dicocokkan sebagai substring terhadap nama lengkap setiap test, jadi cargo test diskon akan menjalankan ketiga test sekaligus karena ketiganya memuat kata diskon. Kedua, kolom yang tadinya 0 filtered out kini menjadi 2 filtered out — dua test yang tidak cocok dengan filter dilaporkan tersaring, bukan hilang. Filter ini bekerja pada nama test setelah prefiks modulnya, sehingga menyaring dengan tests pun akan menjalankan semuanya.
Menguji Fungsi Privat #
Kekuatan yang membedakan unit test Rust dari banyak bahasa lain: modul tests dapat menguji fungsi privat. Aturan visibilitas Rust menyatakan bahwa modul anak selalu bisa melihat item privat milik modul induknya, dan mod tests memang anak dari modul tempat fungsi berada. Karena use super::* mengimpor semua item yang terlihat dari modul induk, fungsi tanpa pub pun ikut masuk scope test. Perhatikan contoh kedua ini — fungsi persen_valid sengaja tidak diberi pub:
pub fn hitung_diskon(harga: u64, persen: u64) -> u64 {
if !persen_valid(persen) {
panic!("persen diskon tidak valid: {}", persen);
}
harga * persen / 100
}
fn persen_valid(persen: u64) -> bool {
persen <= 100
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn persen_valid_menerima_rentang_nol_sampai_seratus() {
assert!(persen_valid(0));
assert!(persen_valid(100));
}
#[test]
fn persen_valid_menolak_nilai_diatas_seratus() {
assert!(!persen_valid(150));
}
}Output yang Dihasilkan:
Compiling kasir v0.1.0 (/tmp/w4e-unit-test-mod-tests/kasir-priv)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.13s
Running unittests src/lib.rs (target/debug/deps/kasir-120bc9ec7040e8c8)
running 2 tests
test tests::persen_valid_menolak_nilai_diatas_seratus ... ok
test tests::persen_valid_menerima_rentang_nol_sampai_seratus ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests kasir
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sPemanggilan persen_valid(0) dan persen_valid(100) pada Baris 18-19 serta persen_valid(150) pada Baris 24 berlangsung sah meskipun fungsi tersebut tidak pub, karena modul tests membacanya lewat jalur modul anak-ke-induk. Ini filosofi penting: unit test menguji unit internal sebuah crate — detail implementasi yang tidak wajib diekspos ke dunia luar — sedangkan menguji lewat antarmuka publik adalah tugas integration test pada Chapter berikutnya.
Ringkasan #
Anatomi unit test Rust berdiri di atas empat komponen: modul tests sebagai penampung, #[cfg(test)] agar modul hanya dikompilasi saat pengujian, use super::* untuk membawa item modul induk ke scope test, dan #[test] untuk mendaftarkan fungsi ke harness. Perintah cargo test menjalankan semua test secara paralel, sedangkan cargo test <nama> menyaring test yang dijalankan berdasarkan kecocokan nama. Karena mod tests adalah modul anak, fungsi privat pun bisa diuji tanpa dipaksa ber-pub. Satu hal masih kurang: bagaimana cara menyatakan ekspektasi di dalam test itu sendiri? Chapter berikutnya mulai dari fondasinya, yaitu macro assert!.