Sejak Section Penanganan Error kita menjauhkan panic dari kode produksi dan memilih Result sebagai bahasa galat yang sopan. Namun di dalam test, semua mekanisme kegagalan yang kita pelajari sejauh ini — assert!, assert_eq!, assert_ne!, hingga #[should_panic] — tetap bekerja lewat panic. Ada satu ketidaknyamanan teknis di balik situasi tersebut: begitu kita menguji fungsi yang mengembalikan Result, kita harus membongkar nilainya dengan match atau memeriksa is_ok()/is_err() sebelum bisa memakai assertion. Rust menawarkan alternatif yang lebih tenang: fungsi test boleh memiliki tipe nilai kembalian Result<(), String>. Test seperti ini dinyatakan lulus ketika mengembalikan Ok(()) dan gagal ketika mengembalikan Err — dan yang paling penting, operator ? menjadi sah dipakai di dalam test, sehingga galat dari fungsi yang diuji bisa dirambatkan langsung menjadi kegagalan test. Chapter ini menguji fungsi validasi_email sederhana dengan dua gaya sekaligus agar kita bisa merasakan perbedaannya.

Cara Kerja Test Ber-Result #

Mekanisme dasarnya sederhana. Test harness bawaan Rust mengevaluasi hasil pemanggilan setiap fungsi test. Untuk test bertipe (), harness hanya mengamati ada tidaknya panic — seperti pada Chapter-Chapter sebelumnya. Untuk test yang mengembalikan Result<(), E> dengan tipe galat apa pun yang mencetak dengan Debug, harness memeriksa nilainya: Ok(()) menandai test lulus, sedangkan Err(e) menandai test gagal dan nilai e dicetak pada bagian laporan kegagalan. Baris test nama_test ... ok tetap tampil di laporan seperti biasa; perbedaannya hanya terjadi di dalam mesin penilaiannya.

Tipe galat paling umum untuk test sehari-hari adalah String, karena pesan galatnya bebas kita tulis. Aturan penulisannya sama seperti Result pada umumnya: tanda tanya ? di akhir baris merambatkan galat, dan Ok(()) menjadi baris penutup yang menyatakan seluruh pemeriksaan berhasil.

Menguji validasi_email dengan Result #

Buat proyek dengan cargo new validasi --lib, lalu isi src/lib.rs berikut:

rust
pub fn validasi_email(email: &str) -> Result<(), String> {
    if email.is_empty() {
        return Err(String::from("email tidak boleh kosong"));
    }
    if !email.contains('@') {
        return Err(format!("email harus mengandung '@': {}", email));
    }
    if email.starts_with('@') || email.ends_with('@') {
        return Err(String::from("tanda '@' harus diapit teks di kedua sisi"));
    }
    Ok(())
}

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

    #[test]
    fn email_benar_lolos_validasi() -> Result<(), String> {
        validasi_email("[email protected]")?;
        Ok(())
    }

    #[test]
    fn email_tanpa_at_ditolak() -> Result<(), String> {
        let hasil = validasi_email("budi.example.com");
        assert!(hasil.is_err());
        Ok(())
    }
}

Jalankan test:

bash
cargo test

Output yang Dihasilkan:

text
   Compiling validasi v0.1.0 (/tmp/w4e-test-dengan-result/validasi)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running unittests src/lib.rs (target/debug/deps/validasi-07594211b669c5ed)

running 2 tests
test tests::email_benar_lolos_validasi ... ok
test tests::email_tanpa_at_ditolak ... ok

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

   Doc-tests validasi

running 0 tests

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

Mari kita bedah kode di atas:

  1. Baris 1-12 (pub fn validasi_email(...)): fungsi produksi bergaya Result penuh — tiga syarat diperiksa berurutan dan tiap pelanggaran mengembalikan Err dengan pesan berbeda. Nilai suksesnya Ok(()) karena fungsi hanya memeriksa, tidak menghasilkan data.
  2. Baris 19 (fn email_benar_lolos_validasi() -> Result<(), String>): signature test yang menjadi inti Chapter ini. Tanda tangan ini menggantikan () bawaan; harness kini menilai test dari nilai kembaliannya.
  3. Baris 20 (validasi_email("[email protected]")?;): operator ? merambatkan kemungkinan galat. Jika fungsi mengembalikan Err, operator ? menghentikan test dan menjadikan Err tersebut nilai kembalian test — test otomatis gagal. Jika Ok, eksekusi lanjut ke baris berikutnya. Inilah kenyamanan utama gaya Result: satu karakter menggantikan blok match pemeriksa manual.
  4. Baris 21 (Ok(())): baris terakhir test tanpa ?, dievaluasi sebagai nilai kembalian fungsi — sinyal resmi bahwa seluruh pemeriksaan lulus.
  5. Baris 26-28 (let hasil = ...; assert!(hasil.is_err());): gaya campuran tetap sah di dalam test ber-Result. Method is_err() milik enum Result memberi bool, lalu assert! menilainya seperti biasa. Penutup Ok(()) tetap wajib karena signature test menuntutnya.

Operator ? juga memudahkan merangkai banyak pemanggilan dalam satu test tanpa bertingkat:

rust
#[test]
fn rangkaian_email_valid() -> Result<(), String> {
    validasi_email("[email protected]")?;
    validasi_email("[email protected]")?;
    Ok(())
}

Dua pemanggilan berjajar seperti dua statement biasa. Bila validasi pertama gagal, ? langsung memutus test dengan pesan galatnya; bila keduanya lolos, Ok(()) menutup test dengan lulus. Gaya penulisan seperti ini terasa alami karena bentuknya persis seperti kode produksi yang memanggil fungsi fallible secara berurutan.

Saat Test Ber-Result Gagal #

Sekarang kita lihat wujud kegagalannya. Test berikut sengaja mengharapkan email kosong lolos validasi — kebalikan dari perilaku fungsi:

rust
#[test]
fn email_kosong_seharusnya_ditolak() -> Result<(), String> {
    validasi_email("")?;
    Ok(())
}

Output yang Dihasilkan:

text
   Compiling validasi v0.1.0 (/tmp/w4e-test-dengan-result/validasi)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.09s
     Running unittests src/lib.rs (target/debug/deps/validasi-07594211b669c5ed)

running 1 test
test tests::email_kosong_seharusnya_ditolak ... FAILED

failures:

---- tests::email_kosong_seharusnya_ditolak stdout ----
Error: "email tidak boleh kosong"


failures:
    tests::email_kosong_seharusnya_ditolak

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`

Ciri khas kegagalan test ber-Result adalah baris Error: "email tidak boleh kosong" — tanpa panicked at, tanpa nomor thread, tanpa lokasi file. Nilai Err dari fungsi mengalir apa adanya menjadi nilai kembalian test, lalu harness mencetaknya dengan format {:?} milik Debug; tanda kutip yang mengapit pesan berasal dari format Debug milik String. Ketidakadaan lokasi baris adalah trade-off yang perlu kita ketahui: laporan ini lebih tenang, tetapi tidak menunjuk assertion mana yang gagal. Pada contoh di atas, pemanggilan validasi_email("") pada Baris 20 dari src/lib.rs yang utuh-lah yang merambatkan galat lewat ?.

Memilih Gaya: Result atau assert #

Kedua gaya bisa dipakai bergantian, bahkan bercampur dalam satu test seperti contoh email_tanpa_at_ditolak. Pedoman praktisnya sebagai berikut. Gunakan tipe kembalian Result ketika fungsi yang diuji sudah mengembalikan Result — operator ? membuat alur test lurus dan pesan galat asli ikut terlaporkan. Gunakan assert! beserta kerabatnya ketika kita membandingkan nilai konkret, karena laporan left/right dari assert_eq! jauh lebih kaya daripada satu baris Error:. Untuk fungsi yang menolak input dengan panic, tetap gunakan #[should_panic] — attribute tersebut berpasangan dengan test bertipe (), bukan Result.

Ringkasan #

Fungsi test boleh mengembalikan Result<(), String>: Ok(()) berarti lulus, Err berarti gagal dengan pesannya tercetak pada baris Error:. Operator ? bekerja penuh di dalam test semacam ini, membuat pemanggilan fungsi fallible berantai terasa seperti kode produksi biasa. Kegagalan gaya Result tidak memuat lokasi baris — informasi yang harus kita cari sendiri dari isi test. Dengan semua gaya unit test sudah di tangan, saatnya melangkah keluar dari crate: integration test menguji kode kita sebagaimana pengguna luar memakainya, dari direktori tests yang terpisah.