Lewati ke konten

Testing di Rust

Rust punya fitur testing (pengujian) yang sudah tertanam langsung di bahasanya dan di cargo, jadi kamu tidak perlu memasang library terpisah untuk mulai. Halaman ini membahas secukupnya agar kamu bisa menulis dan menjalankan test pertamamu.

Test adalah fungsi biasa yang diberi tanda #[test]:

fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
}

Ada beberapa hal yang perlu diperhatikan:

  • #[cfg(test)] memberi tahu Rust bahwa modul ini hanya dikompilasi saat test dijalankan. Modul ini bukan bagian dari program normalmu.
  • use super::*; membawa semua isi modul induk (di sini, fungsi add) ke dalam cakupan, supaya test bisa memanggilnya.
  • #[test] menandai sebuah fungsi sebagai test yang harus dijalankan oleh cargo test.

Dari folder proyekmu:

Terminal window
cargo test

Kamu akan melihat output seperti ini:

running 1 test
test tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignored

Cargo menulis ringkasan ini dalam bahasa Inggris: artinya 1 test dijalankan, it_adds_two_numbers berhasil (ok), dan hasilnya 1 lulus, 0 gagal, 0 dilewati.

Tiga makro ini sudah mencakup hampir semua yang kamu butuhkan:

#[test]
fn checks() {
assert!(2 + 2 == 4); // test gagal jika kondisinya false
assert_eq!(2 + 2, 4); // gagal jika kedua nilai tidak sama
assert_ne!(2 + 2, 5); // gagal jika kedua nilai JUSTRU sama
}

Biasanya assert_eq! dan assert_ne! lebih disukai daripada assert!(a == b) biasa. Saat gagal, keduanya mencetak kedua nilai, jadi kamu langsung tahu apa yang tidak cocok, bukan sekadar pesan “assertion failed” (pengecekan gagal).

Test yang gagal, dan seperti apa tampilannya

Section titled “Test yang gagal, dan seperti apa tampilannya”
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_will_fail() {
assert_eq!(add(2, 2), 5);
}
}
test tests::this_will_fail ... FAILED
assertion `left == right` failed
left: 4
right: 5

Pesan seperti inilah yang kamu inginkan saat test gagal. left (kiri) adalah nilai yang benar-benar dihasilkan kodemu, dan right (kanan) adalah nilai yang diharapkan test. Sekilas saja kamu sudah tahu apa yang keluar dan apa yang seharusnya keluar.

Kadang perilaku yang benar justru sebuah panic (program berhenti karena error). Misalnya, fungsi yang harus menolak input yang tidak valid. Untuk menguji hal itu ada #[should_panic]:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("tidak bisa membagi dengan nol");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn dividing_by_zero_panics() {
divide(10, 0);
}
}

Saat test gagal, kamu ingin tahu kenapa secepat mungkin. Dua tambahan kecil ini sangat membantu.

Pesan sendiri. assert!, assert_eq!, dan assert_ne! semuanya menerima pesan tambahan setelah nilai-nilainya, ditulis persis seperti di println!. Pesan itu hanya dicetak kalau pengecekannya gagal.

expected pada should_panic. #[should_panic] saja akan lulus untuk panic apa pun, bahkan panic yang disebabkan bug lain. Dengan menambahkan expected = "...", test hanya lulus kalau pesan panic-nya mengandung teks tersebut.

Ini fungsi divide tadi lagi, dengan main supaya bisa dijalankan, dan dua test yang memakai kedua ide itu:

fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("tidak bisa membagi dengan nol");
}
a / b
}
fn main() {
println!("10 / 2 = {}", divide(10, 2));
println!("7 / 2 = {}", divide(7, 2));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rounds_down() {
let result = divide(7, 2);
assert_eq!(result, 3, "7 / 2 seharusnya dibulatkan ke bawah, tapi hasilnya {result}");
}
#[test]
#[should_panic(expected = "membagi dengan nol")]
fn dividing_by_zero_panics() {
divide(10, 0);
}
}

Output:

10 / 2 = 5
7 / 2 = 3
▶ Coba sendiri

Output di atas adalah yang dicetak oleh cargo run, karena main tidak menjalankan test. Untuk menjalankan test, pakai cargo test. Di Rust Playground, buka menu di sebelah tombol Run lalu pilih Test. Kamu akan melihat:

running 2 tests
test tests::rounds_down ... ok
test tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Test dijalankan secara paralel, jadi dua baris test ... bisa muncul dengan urutan mana saja. Sekarang bayangkan test-nya mengharapkan 4, bukan 3. Pesan kegagalannya akan memuat pesanmu, tepat di sebelah kedua nilai:

assertion `left == right` failed: 7 / 2 seharusnya dibulatkan ke bawah, tapi hasilnya 3
left: 3
right: 4

Dan kalau divide panic dengan pesan yang berbeda, pengecekan expected akan menangkapnya. Rust memberi tahu (dalam bahasa Inggris) bahwa pesan panic tidak mengandung teks yang diharapkan, lalu menampilkan keduanya:

note: panic did not contain expected string
panic message: "tidak bisa membagi dengan nol"
expected substring: "negatif"

Jawaban jujurnya: untuk skrip pendek, mungkin kamu tidak akan menulisnya. Test mulai terasa manfaatnya ketika kode akan dipakai ulang atau diubah nanti. Test membuatmu berani merapikan kode (refactor), karena cargo test langsung memberi tahu kalau ada yang rusak, daripada kamu baru tahu dari pengguna.

Test juga bisa ada di dalam dokumentasimu: contoh kode di dalam komentar dokumentasi /// ikut dijalankan oleh cargo test, jadi contoh-contohmu tidak akan pernah ketinggalan zaman. Halaman Dokumentasi menunjukkan caranya.

Untuk kedua latihan, solusinya punya main yang bisa kamu jalankan, dan test yang dijalankan dengan cargo test (atau Test di Playground).

Tulis fn grade(score: u32) -> char yang mengembalikan 'A' untuk 90 ke atas, 'B' untuk 80 sampai 89, 'C' untuk 70 sampai 79, dan 'F' untuk nilai di bawahnya. Lalu tulis test yang memeriksa batas-batasnya, seperti 90 dan 89. Petunjuk: bug suka bersembunyi di batas. Test untuk tepat 90 akan menangkap kesalahan klasik menulis > padahal seharusnya >=.

Lihat solusi
fn grade(score: u32) -> char {
if score >= 90 {
'A'
} else if score >= 80 {
'B'
} else if score >= 70 {
'C'
} else {
'F'
}
}
fn main() {
for score in [95, 85, 72, 40] {
println!("{score} -> {}", grade(score));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edges_of_a() {
assert_eq!(grade(90), 'A');
assert_eq!(grade(89), 'B');
}
#[test]
fn edges_of_c() {
assert_eq!(grade(70), 'C');
assert_eq!(grade(69), 'F');
}
#[test]
fn zero_is_f() {
assert_eq!(grade(0), 'F', "skor 0 seharusnya tidak lulus");
}
}

Output:

95 -> A
85 -> B
72 -> C
40 -> F
▶ Coba sendiri

cargo test seharusnya melaporkan 3 passed. Coba ubah score >= 90 menjadi score > 90 lalu jalankan test lagi: edges_of_a akan gagal dan menunjukkan left: 'B' dan right: 'A'.

Tulis fn safe_divide(a: i32, b: i32) -> Option<i32> yang mengembalikan None saat b bernilai 0, alih-alih panic. Tulis satu test untuk pembagian normal dan satu untuk pembagian dengan nol. Petunjuk: assert_eq! juga bisa dipakai dengan Option, jadi kamu bisa menulis assert_eq!(safe_divide(1, 0), None).

Lihat solusi
fn safe_divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
fn main() {
println!("{:?}", safe_divide(10, 2));
println!("{:?}", safe_divide(1, 0));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn divides_normally() {
assert_eq!(safe_divide(10, 2), Some(5));
}
#[test]
fn zero_gives_none() {
assert_eq!(safe_divide(1, 0), None);
}
}

Output:

Some(5)
None
▶ Coba sendiri

Bandingkan dengan fungsi divide di atas. Di sana, membagi dengan nol menghasilkan panic, jadi test-nya butuh #[should_panic]. Di sini, hasilnya hanyalah nilai kembalian biasa, jadi assert_eq! biasa sudah cukup. Fungsi yang mengembalikan Option atau Result biasanya lebih mudah diuji.

  • Tandai sebuah fungsi dengan #[test] untuk menjadikannya test; jalankan semua test dengan cargo test.
  • assert!, assert_eq!, dan assert_ne! sudah mencakup sebagian besar pengecekan sehari-hari. Utamakan assert_eq!/assert_ne! karena pesan kegagalannya lebih jelas.
  • #[should_panic] menguji bahwa sebuah fungsi memang panic saat seharusnya. Tambahkan expected = "..." untuk memastikan panic-nya terjadi karena alasan yang benar.
  • Tambahkan pesan sendiri ke makro assert mana pun untuk menjelaskan apa yang salah saat gagal.
  • Test paling berguna untuk kode yang kamu perkirakan akan diubah atau dipakai ulang nanti.

Kuis singkat

1. Atribut apa yang menandai sebuah fungsi sebagai test yang harus dijalankan oleh cargo test?

2. Kenapa assert_eq! dan assert_ne! biasanya lebih disukai daripada assert!(a == b) biasa?

3. Apa fungsi #[should_panic]?

Skor: 0 / 3