跳过正文

测试:testing

··1402 字
Rust
rust-lang - 这篇文章属于一个选集。
§ 19: 本文

Rust 提供了如下测试类型:

  1. 单元测试:Unit testing;
  2. 文档测试:Doc testing;
  3. 集成测试:Integration testing;

Cargo.toml 为 testing/exampels 提供了单独的构建时依赖配置:

[dev-dependencies]
pretty_assertions = "1"

单元测试 Unit testing:

  1. 使用 #[cfg(test)] 注解 test module ,该 module 在 cargo build 时会被忽略(减少代码体积),而只有运行 cargo test 时才被编译和执行;
  2. 使用 #[test] 注解要测试的函数,test 函数内部使用 assert!()/assert_eq!()/assert_ne!()/Result 等来报告错误,assert 宏支持格式化字符串消息;
  3. 单元测试的代码和源码在同一个文件或目录, 可以用来测试非 public 的接口;
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
    a - b
}

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

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn test_bad_add() {
        assert_eq!(bad_add(1, 2), 3);
    }
}

Rust 2018 版本的单元测试函数支持返回 Result 来报错, 故可以使用 ?:

fn sqrt(number: f64) -> Result<f64, String> {
    if number >= 0.0 {
        Ok(number.powf(0.5))
    } else {
        Err("negative floats don't have square roots".to_owned())
    }
}

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

    #[test]
    fn test_sqrt() -> Result<(), String> {
        let x = 4.0;
        assert_eq!(sqrt(x)?.powf(2.0), x);
        Ok(())
    }
}

单元测试也支持匹配 panic!() , 使用 expected 匹配 panic 消息字符串;

pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
    if b == 0 {
        panic!("Divide-by-zero error");
    } else if a < b {
        panic!("Divide result is zero");
    }
    a / b
}

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

    #[test]
    fn test_divide() {
        assert_eq!(divide_non_zero_result(10, 2), 5);
    }

    #[test]
    #[should_panic]
    fn test_any_panic() {
        divide_non_zero_result(1, 0);
    }

    #[test]
    #[should_panic(expected = "Divide result is zero")]
    fn test_specific_panic() {
        divide_non_zero_result(1, 10);
    }
}

为 test 函数添加 #[ignore] 来忽略测试,但可以使用 cargo test -- --ignored 来只运行这些被忽略的测试。

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    fn test_add_hundred() {
        assert_eq!(add(100, 2), 102);
        assert_eq!(add(2, 100), 102);
    }

    #[test]
    #[ignore]
    fn ignored_test() {
        assert_eq!(add(0, 0), 0);
    }
}

文档测试:在 Rust 的注释中(markdown 语法)嵌入嵌入代码块, 在编译和测试时会自动运行这些代码:

/// First line is a short summary describing function.
///
/// The next lines present detailed documentation. Code blocks start with
/// triple backquotes and have implicit `fn main()` inside
/// and `extern crate <cratename>`. Assume we're testing `doccomments` crate:
///
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Usually doc comments may include sections "Examples", "Panics" and "Failures".
///
/// The next function divides two numbers.
///
/// # Examples
///
/// ```
/// let result = doccomments::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// The function panics if the second argument is zero.
///
/// ```rustshould_panic
/// // panics on division by zero
/// doccomments::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Divide-by-zero error");
    }

    a / b
}

集成测试:

  • 集成测试位于被测试 crate 外部,目的是测试被测试库的多个部分能否正确的一起工作,所以覆盖率很重要;
  • 集成测试位于 tests 目录下,各文件对应一个单独的集成测试,可以包含子 module,只能测试 public 接口;
// common 是各集成测试可以复用的 module,这样可以把测试公共的接口拆分为公共 module
// tests/common/mod.rs:
pub fn setup() {
    // some setup code, like creating required files/directories, starting servers, etc.
}

// tests/integration_test.rs importing common module.
mod common;

// 集成测试会被单独编译为 binary 并执行,故不再需要 #cfg[test] 注解。
#[test]
fn test_add() {
    // using common code.
    common::setup();
    assert_eq!(adder::add(3, 2), 5);
}

使用 cargo test 命令运行测试:

  1. 默认使用 多线程并发运行 所有测试(单元+集成),同时也会运行文档注释中的代码块;
  2. cargo test 会生成一个可执行程序 ,所以为 cargo test 指定参数时,包含两部分:
    1. cargo test 命令本身的参数:直接放到 test 命令后;
    2. 编程生成的 test 程序的参数:放到 – 分割符号后,如 cargo test – –ignored;

例如:

  1. 运行匹配关键字的特殊函数/module 名称(只能指定一个关键字): cargo test KEYWORD
  2. 只运行指定文件中的集成测试: cargo test --test integrated_test_file_name
  3. 单线程运行测试任务: cargo test -- --test-threads=1
  4. test 默认捕获 println!() 输出,只有当 test 出错时才显示。指定 cargo test -- --nocapture 显示所有终端输出。
  5. 只运行被忽略的测试: cargo test -- --ignored
$ cargo test

running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok

failures:

---- tests::test_bad_add stdout ----
        thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
  left: `-1`,
 right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.


failures:
    tests::test_bad_add

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out


$ cargo test
running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests doccomments

running 3 tests
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 21) ... ok
test src/lib.rs - div (line 31) ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

如果项目只有 binary 而没有 lib,则不能运行集成测试,解决办法是将 binary 中的逻辑拆解到 lib 中。

对于 tokio 单元或集成测试,需要使用 #[tokio::test] 代替 #[test] 属性宏,前者会创建一个单线程的异步运行时来执行测试:

#[cfg(test)]
mod tests {
    use crate::health_check;

    #[tokio::test]
    async fn health_check_succeeds() {
        let response = health_check().await;
        // This requires changing the return type of `health_check` from `impl Responder` to
        // `HttpResponse` to compile You also need to import it with `use
        // actix_web::HttpResponse`!
        assert!(response.status().is_success())
    }
}

参考:

  1. Everything you need to know about testing in Rust
rust-lang - 这篇文章属于一个选集。
§ 19: 本文

相关文章

不安全:unsafe
··824 字
Rust
Rust
借用:refer/borrow
··3127 字
Rust
Rust 引用类型和借用
函数、方法和闭包:function/method/closure
··7032 字
Rust
Rust 函数、方法和闭包
包和模块:package/crate/module
··2066 字
Rust
Rust 项目的包和模块组织结构