Kiểm thử trong Rust
Rust tích hợp sẵn việc kiểm thử (testing) vào ngay ngôn ngữ và cargo, nên bạn không cần tìm thêm thư viện riêng nào để bắt đầu. Trang này trình bày vừa đủ để bạn viết và chạy những test đầu tiên.
Viết một test cơ bản
Phần tiêu đề “Viết một test cơ bản”Một test là một hàm bình thường được đánh dấu bằng #[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); }}Vài điều đáng chú ý:
#[cfg(test)]báo cho Rust biết module này chỉ được biên dịch khi chạy test; nó không thuộc chương trình bình thường của bạn.use super::*;đưa mọi thứ từ module cha (ở đây là hàmadd) vào, để test gọi được nó.#[test]đánh dấu một hàm cụ thể là test màcargo testsẽ chạy.
Chạy test
Phần tiêu đề “Chạy test”Từ thư mục dự án của bạn:
cargo testBạn sẽ thấy kết quả kiểu như:
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredKết quả do cargo test in ra luôn bằng tiếng Anh: ở đây 1 test đã chạy và đã qua (passed).
assert! và những người bạn
Phần tiêu đề “assert! và những người bạn”Ba macro này đáp ứng phần lớn những gì bạn cần:
#[test]fn checks() { assert!(2 + 2 == 4); // test thất bại nếu điều kiện là false assert_eq!(2 + 2, 4); // thất bại nếu hai giá trị không bằng nhau assert_ne!(2 + 2, 5); // thất bại nếu hai giá trị LẠI bằng nhau}Người ta thường thích assert_eq! và assert_ne! hơn assert!(a == b) trần: khi thất bại, chúng in ra cả hai giá trị, nên bạn thấy ngay cái gì không khớp, thay vì chỉ nhận được câu “assertion failed”.
Một test thất bại trông như thế nào
Phần tiêu đề “Một test thất bại trông như thế nào”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Đó chính là kiểu thông báo bạn muốn có khi một test thất bại: left là giá trị code của bạn thật sự tạo ra, còn right là giá trị mà test mong đợi.
Kiểm tra rằng một hàm panic
Phần tiêu đề “Kiểm tra rằng một hàm panic”Đôi khi hành vi đúng chính là panic, ví dụ một hàm phải từ chối dữ liệu đầu vào không hợp lệ. #[should_panic] kiểm tra điều đó:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("cannot divide by zero"); } a / b}
#[cfg(test)]mod tests { use super::*;
#[test] #[should_panic] fn dividing_by_zero_panics() { divide(10, 0); }}Thông báo lỗi dễ hiểu hơn
Phần tiêu đề “Thông báo lỗi dễ hiểu hơn”Khi một test thất bại, bạn muốn biết vì sao càng nhanh càng tốt. Hai bổ sung nhỏ giúp ích rất nhiều.
Một thông báo tự viết. assert!, assert_eq! và assert_ne! đều nhận thêm một thông báo sau các giá trị, viết y như println!. Nó chỉ được in ra khi phép kiểm tra thất bại.
expected trong should_panic. Một #[should_panic] trần sẽ qua với bất kỳ panic nào, kể cả panic do một bug khác gây ra. Thêm expected = "..." khiến test chỉ qua khi thông báo panic có chứa đoạn văn bản đó.
Đây lại là hàm divide, có thêm main để bạn chạy được, và hai test dùng cả hai ý tưởng trên. Thông báo panic và thông báo trong assert được giữ bằng tiếng Anh vì chúng xuất hiện nguyên văn trong kết quả test bên dưới:
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("cannot divide by zero"); } 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 should round down, but got {result}"); }
#[test] #[should_panic(expected = "divide by zero")] fn dividing_by_zero_panics() { divide(10, 0); }}Kết quả:
10 / 2 = 57 / 2 = 3Kết quả ở trên là những gì cargo run in ra, vì main không chạy các test. Để chạy test, hãy dùng cargo test. Trong Rust Playground, mở menu cạnh nút Run và chọn Test. Bạn sẽ thấy:
running 2 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered outCác test chạy song song, nên hai dòng test ... có thể hiện ra theo thứ tự bất kỳ. Giờ hãy tưởng tượng test mong đợi 4 thay vì 3. Lỗi sẽ có kèm thông báo của bạn, ngay cạnh hai giá trị:
assertion `left == right` failed: 7 / 2 should round down, but got 3 left: 3 right: 4Và nếu divide panic với một thông báo khác, phép kiểm tra expected sẽ bắt được (thông báo nói rằng panic không chứa chuỗi mong đợi):
note: panic did not contain expected string panic message: "cannot divide by zero" expected substring: "negative"Vì sao nên viết test
Phần tiêu đề “Vì sao nên viết test”Câu trả lời thật lòng: với một đoạn script ngắn, có lẽ bạn sẽ không viết. Test bắt đầu đáng giá khi code sẽ được dùng lại hoặc thay đổi về sau: chúng cho phép bạn tái cấu trúc (refactor) một cách tự tin, vì cargo test sẽ báo ngay nếu bạn làm hỏng gì đó, thay vì để người dùng phát hiện ra.
Test cũng có thể nằm trong tài liệu của bạn: các ví dụ code trong chú thích tài liệu /// cũng chạy cùng cargo test, nên ví dụ của bạn không bao giờ bị lỗi thời. Trang Tài liệu chỉ cho bạn cách làm.
Luyện tập
Phần tiêu đề “Luyện tập”Với cả hai bài tập, lời giải có một hàm main để bạn chạy, và các test để bạn chạy bằng cargo test (hoặc Test trong Playground).
1. Kiểm thử hàm xếp loại
Phần tiêu đề “1. Kiểm thử hàm xếp loại”Viết fn grade(score: u32) -> char trả về 'A' cho điểm từ 90 trở lên, 'B' cho 80 đến 89, 'C' cho 70 đến 79, và 'F' cho mọi điểm thấp hơn. Sau đó viết test kiểm tra các điểm biên, như 90 và 89. Gợi ý: biên là nơi bug hay ẩn nấp. Một test cho đúng điểm 90 sẽ bắt được lỗi kinh điển là viết > thay vì >=.
Xem lời giải
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', "điểm 0 phải bị xếp loại F"); }}Kết quả:
95 -> A85 -> B72 -> C40 -> Fcargo test sẽ báo 3 passed. Hãy thử đổi score >= 90 thành score > 90 rồi chạy lại test: edges_of_a thất bại và cho bạn thấy left: 'B' và right: 'A'.
2. Kiểm thử một hàm trả về Option
Phần tiêu đề “2. Kiểm thử một hàm trả về Option”Viết fn safe_divide(a: i32, b: i32) -> Option<i32> trả về None khi b bằng 0 thay vì panic. Viết một test cho phép chia bình thường và một test cho phép chia cho 0. Gợi ý: assert_eq! cũng dùng được với Option, nên bạn có thể viết assert_eq!(safe_divide(1, 0), None).
Xem lời giải
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); }}Kết quả:
Some(5)NoneHãy so sánh với hàm divide ở trên. Ở đó, chia cho 0 là một panic, nên test cần #[should_panic]. Ở đây nó chỉ là một giá trị trả về bình thường, nên một assert_eq! đơn giản là đủ. Các hàm trả về Option hoặc Result thường dễ kiểm thử hơn.
Tóm tắt
Phần tiêu đề “Tóm tắt”- Đánh dấu một hàm bằng
#[test]để biến nó thành test; chạy mọi test bằngcargo test. assert!,assert_eq!vàassert_ne!đáp ứng phần lớn các phép kiểm tra hằng ngày; nên ưu tiênassert_eq!/assert_ne!để có thông báo lỗi rõ hơn.#[should_panic]kiểm tra rằng một hàm panic khi nó phải panic. Thêmexpected = "..."để kiểm tra rằng nó panic đúng lý do.- Thêm thông báo tự viết vào bất kỳ macro assert nào để giải thích điều gì đã sai khi nó thất bại.
- Test có giá trị nhất với code mà bạn dự định thay đổi hoặc dùng lại về sau.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Thuộc tính nào đánh dấu một hàm là test mà cargo test sẽ chạy?
2. Vì sao người ta thường ưu tiên assert_eq! và assert_ne! hơn assert!(a == b) trần?
3. #[should_panic] làm gì?
Điểm: 0 / 3
Đọc thêm
Phần tiêu đề “Đọc thêm”- How to Write Tests — The Rust Book (tiếng Anh):
#[test], các macro assert, thông báo tự viết,should_panicvà test trả vềResult. - Controlling How Tests Are Run — The Rust Book (tiếng Anh): chạy một test theo tên, hiện kết quả
println!, và#[ignore]. - Test Organization — The Rust Book (tiếng Anh): khác biệt giữa unit test và integration test trong thư mục
tests/. - Unit testing — Rust by Example (tiếng Anh): các ví dụ ngắn về test qua, test thất bại và test panic.
cargo test— The Cargo Book (tiếng Anh): mọi tùy chọn của lệnhcargo test.