Tổ chức Tests

Testing là một kỹ năng phức tạp. Nhiều lập trình viên sử dụng nhiều thuật ngữ khác nhau và tổ chức code tests khác nhau. Cộng đồng Rust đặt ra hai loại tests:

  • Unit tests: nhỏ và tập trung vào một function, module độc lập tại một thời điểm.
  • Integration tests: tests nằm ngoài thư viện của bạn, sử dụng thư viện của bạn để tests như thể là một chương trình thực tế sẽ thực sự sử dụng. Chỉ test trên public interface và tập trung vào cả 1 module cho một test.

Unit Tests

Unit tests được đặt trong thư mục src trong mỗi file code mà bạn đang test. Có một convention là tạo một module tên tests trong mỗi file chứa function cần test, annotate module này với attribute #[cfg(test)].

#[cfg(test)]

#[cfg(test)] báo cho compiler biết module này được dùng để compile thành test, chỉ được dùng khi chạy cargo test, không dùng khi cargo build.

File: src/lib.rs

#![allow(unused)]
fn main() {
pub fn adder(a: i32, b: i32) -> i32 {
    a + b
}

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

  #[test]
  fn test_adder() {
    let expected = 4;
    let actual = adder(2, 2);

    assert_eq!(expected, actual);
  }
}
}

Trong module tests có thể sẽ có một vài helper function khác, do đó những function nào là function test sẽ được đánh dấu là #[test] để compiler nhận biết.

Test private function

Ở ví dụ trên thì public function adder được import vào trong module tests theo đúng rule của Rust. Vậy còn private function thì sao? Có một cuộc tranh cãi trong cộng đồng về việc này. Cuối cùng thì Rust cho phép import private function vào tests module.

#![allow(unused)]
fn main() {
pub fn adder(a: i32, b: i32) -> i32 {
    adder_internal(a, b)
}

fn adder_internal(a: i32, b: i32) -> i32 {
    a + b
}
 
#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_adder() {
    let expected = 4;
    let actual = adder_internal(2, 2);

    assert_eq!(expected, actual);
  }
}
}

Integration Tests

Integration tests nằm hoàn toàn bên ngoài thư viện của bạn. Mục đích của integration tests nhằm kiểm tra các thành phần của thư viện của bạn hoạt động cùng với nhau có chính xác không.

Để bắt đầu viết integration tests, tạo thư mục tests nằm cùng cấp với src.

Trong thư mục tests, cargo sẽ compile mỗi file thành một thành một crate độc lập.

File: tests/integration_test.rs

#![allow(unused)]
fn main() {
use adder;

#[test]
fưn it_adds_two() {
  assert_eq!(4, adder::adder(2, 2));
}
}

Ở đây chúng ta import use adder thay vì use crate:: do file integration test này là một crate độc lập.

Chạy cargo test

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished test [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::test_adder ... ok

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

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

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

   Doc-tests adder

running 0 tests

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

Do integration tests phải import thư viện để chạy test code, crate bắt buộc phải có src/lib.rs. Các binary crates không thể test theo cách này.

Do đó các project Rust họ thường tổ chức theo kiểu build binary từ src/main.rs và import trực tiếp logic từ src/lib.rs.

References

Doc Tests

Rust cũng hỗ trợ execute code ví dụ trên document như là một test. Đây là giải pháp cực kỳ thông minh giúp đảm bảo example code luôn up to date và nó có hoạt động.

#![allow(unused)]
fn main() {
/// Function that adding two number
///
/// # Example
///
/// ```
/// use adder::adder;
/// 
/// assert_eq!(4, adder(2, 2));
/// ```
pub fn adder(a: i32, b: i32) -> i32 {
    a + b
}
}

Khi chạy cargo test hoặc cargo test --doc, cargo sẽ compile phần example code này thành một crate test và thực thi nó.

Mặc định nếu không chỉ định ngôn ngữ cho block code thì rustdoc sẽ ngầm định nó là Rust code. Do đó

```
let x = 5;
```

sẽ tương đương với

```rust
let x = 5;
```

Ẩn một phần của example code

Đôi lúc bạn sẽ cần example code gọn hơn, ẩn bớt một số logic mà bạn chuẩn bị để code có thể chạy được, tránh làm distract của người xem.

/// ```
/// /// Some documentation.
/// # fn foo() {} // this function will be hidden
/// println!("Hello, World!");
/// ```

Chúng ta thêm # ở phần đầu của dòng code muốn ẩn đi trong generate doc, nó vẫn sẽ được compile như mình thường.

Sử dụng ? trong doc tests

? chỉ có thể được sử dụng khi function trả về Result<T, E>. Hãy sử dụng cách sau:

/// A doc test using ?
///
/// ```
/// use std::io;
/// fn main() -> io::Result<()> {
///     let mut input = String::new();
///     io::stdin().read_line(&mut input)?;
///     Ok(())
///  }
/// ```

Cùng với việc sử dụng # như ở trên, chúng ta có thể ẩn đi bớt logic.

/// A doc test using ?
///
/// ```
/// # use std::io;
/// # fn main() -> io::Result<()> {
/// let mut input = String::new();
/// io::stdin().read_line(&mut input)?;
/// # Ok(())
/// # }
/// ```

References