Away0x's Blog

Coding blogging for hackers.

Rust - Test

  • Rust 中一个测试就是一个函数, 需要使用 test 属性进行标注
  • 使用 cargo test 命令运行所有测试函数
    • Rust 会构建一个 Test Runner 可执行文件,它会运行标注了 test 的函数,并报告其运行是否成功

如何运行测试

1
2
3
4
5
6
7
8
9
10
11
# 并行运行所有测试函数
# - 测试成功时: 不显示所有输出(如 println!),使读取与测试结果相关的输出更容易
# - 测试失败时会显示如 println! 这样的输出
cargo test

# 命令行参数分为以下两大类
# 1. 针对 cargo test 的参数: 紧跟 cargo test 之后
cargo test --help # 输出 cargo test 的可用的参数

# 2. 针对测试可执行程序: 放在 -- 之后
cargo test -- --help # 输出 -- 之后的所有可用参数
  • 并行运行测试: 默认使用多个线程并行运行
    • 速度快
    • 要考虑并发影响,需要确保测试之间不会互相依赖,并且步依赖于某个共享状态(环境、工作目录、环境变量等等)
1
2
3
4
5
# --test-threads 参数
# - 传递给二进制文件
# - 🙅以并行方式运行测试,或向对线程数进行细粒度控制
# - 可使用该参数,后面跟线程的数量
cargo test -- --test-threads=1 # 单线程运行测试
  • 显示输出
    • 默认情况下, 测试通过,Rust 的 test 库会捕获所有打印到标准输出的内容 (如 println!)
1
2
# 即使测试通过,也显示标准输出
cargo test -- --show-output
  • 选择性的运行测试
1
2
3
4
# 根据测试函数的名称指定运行的测试
cargo test test_one_fn # 运行单个测试
# 可指定测试名的一部分 (模块名也可以) 来匹配并运行多个测试
cargo test test_ # 会运行测试函数名字中带有 test_ 的测试

如何编写测试

1
2
#[test]
fn test_fn() {}
  • assert! 宏,来自标准库,用来确定某个状态是否为 true
    • true: 测试通过
    • false: 调用 panic!,测试失败
  • 使用 assert_eq!assert_ne! 测试相等性
    • 判断两个参数是否相等/不等
    • 实际上,它们使用的就是 ==!= 运算符
    • 断言失败: 自动打印出两个参数的值 (要求参数实现了 PartialEq 和 Debug Traits)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#[derive(Debug)]
pub struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    pub fn can_hold(&self, other: &Rectangle) -> bool {
        self.length > other.length && self.width > other.width
    }
}

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

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectangle { length: 8, width: 7 };
        let smaller = Rectangle {
            length: 5,
            width: 1,
        };

        assert!(larger.can_hold(&smaller)); // 二参可传自定义信息
        // assert!(something, "{} {}", formart_param1, formart_param2);
    }
}
  • 使用 should_panic 属性测试是否发生了恐慌 (验证代码是否发生了 panic)
1
2
3
4
5
6
7
8
9
10
11
// 函数 panic: 测试通过,否则失败
#[cfg(test)]
mod tests {
    use super::*;

    // should_panic 修饰的函数 panic 了测试才会通过
    // 可 should_panic(expected = "xxxx"),验证 panic 时,错误信息是否包含了 expected 注明的字符串
    #[test]
    #[should_panic]
    fn test_fn() {...}
}
  • 测试中使用 Result<T, E>
    • 无需 panic, 可使用 Result<T, E> 作为返回类型编写测试
    • 返回 Ok 测试通过,返回 Err 测试失败
1
2
3
4
5
6
7
8
9
10
11
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(panic!(String::from("two plus two does not equal four")))
        }
    }
}
  • 忽略某些测试,运行剩余的测试
    • 使用 ignore 属性进行标记
1
2
3
4
5
6
7
8
9
10
11
// 运行 cargo test,只会运行 fn1 这个测试
// "cargo test -- --ignored": 只运行被标记 ignore 的测试
#[cfg(test)]
mod tests {
    #[test]
    fn fn1() {...}

    #[test]
    #[ignore]
    fn fn2() {...}
}

如何组织测试

单元测试

  • 一次对一个模块进行隔离的测试
  • 可测试 private 接口
  • 一般单元测试和被测试的代码都放在 src 目录下的同一个文件中
  • 约定每个源代码文件都建立 tests 模块来放测试函数,并使用 #[cfg(test)] 标注 tests 模块
    • 使用 #[cfg(test)] 标注后,只有运行 cargo test 才编译和运行代码,而 cargo build 则不会
    • cfg: Configuration, 告诉 Rust 下面的条目只有在指定的配置选项下才被包含
1
2
3
4
5
6
7
8
9
10
11
12
13
// 只在 cargo test 才会把以下代码拉入编译范围
fn fn1() {...}

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

    #[test]
    fn it_works() {
        fn1(); // 可调用私有函数
        assert_eq!(4, 2 + 2);
    }
}

集成测试

  • 在库外部,和其他外部代码一样使用你的代码
  • 只能访问 public 接口
  • 可能在每个测试中使用到多个模块
  • 集成测试和被测试文件在不同的目录,不需要 #[cfg(test)] 标注
  • 集成测试放在 src 同级的 tests 目录下,tests 目录下的每个测试文件都是单独的一个 crate
    • 这些文件步共享行为 (与 src 下的文件规则不同)
    • 如果需要在 tests 文件下共享逻辑,可以建立子目录,在其中编写通用逻辑 (tests 下的子目录不会被当成测试文件运行)
1
2
3
4
5
6
7
8
// adder/tests/integration_test.rs
use adder; // 项目的名字

// 由于 tests 目录只会在执行 cargo test 命令的时候运行,所以不需要使用 #[cfg(test)] 标注
#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2))
}
  • 运行指定的集成测试 cargo test 测试函数名
  • 运行某个测试文件内的所有测试: cargo test --test 文件名

针对 binary crate 的集成测试

  • 如果项目时 binary crate,只含有 src/main.rs 没有 src/lib.rs
    • 不能在 tests 目录下创建集成测试
    • tests 无法把 main.rs 的函数导入作用域
  • 只有 library crate 才能暴露函数给其他 crate 用
  • binary crate 意味着独立运行
  • 所以通常 binary crate,都会把逻辑放在 lib.rs 里面,方便集成测试,main.rs 只有少量的调用逻辑