Rust 学习笔记 - 测试

在 Rust 中一个测试就是一个函数,他被用于验证非测试代码的功能是否和预期一致。

在测试的函数体里面通常会执行 3 个操作(简称 3A 操作):

  • 准备(Arrange)数据/状态
  • 运行(Act)被测试的代码
  • 断言(Assert)结果

测试函数

测试函数本质也是函数,只是需要使用 test 属性(attribute)进行标注或者叫修饰。

Attribute 就是一段 Rust 代码的元数据,在函数上加 #[test],可以把函数变成测试函数。

运行测试

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

使用 cargo 创建 library 项目的时候,会生成一个 test module,里面有一个 test 函数,你可以添加任意数量的 test module 或函数。

例子

创建一个 library 项目:

cargo new adder --lib
复制代码

在其 src/lib.rs 中有一段默认的测试代码:

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }
}
复制代码

进入目录

cd adder
复制代码

运行测试

cargo test
复制代码

断言(Assert)

assert! 宏,来自标准库,用来确定某个状态是否为 true,true 为通过测试,false 会调用 panic!,测试失败。

assert_eq! 宏,来自标准库,判断两个参数是否相等,类似 ==

assert_ne! 宏,来自标准库,判断两个参数是否不等,类似 !=

assert_eq!assert_ne! 在断言失败的时候会自动打印出两个参数的值,使用 debug 格式打印参数要求参数实现了 PartialEqDebug Traits(所有的基本类型和标准库里大部分类型都实现了,不过对于自定义的枚举和结构体就需要自己实现这两个 Trait 了)。

添加自定义错误消息

assert! 的第二参数,assert_eq!assert_ne! 的第三参数是可选的,可以传入自定义消息,失败的时候会打印出来,自定义消息参数会被传递给 format! 宏,可以使用 {} 占位符。

验证错误处理的情况

测试除了验证代码返回是否正确,还需验证代码是否如预期的处理了发生错误的情况。可验证代码在特定情况下是否发生了 panic。

需要添加额外的 attribute should_panic。添加 expected 参数可以更加精准的检查出失败信息中包含的文字。

#[cfg(test)]
mod tests {
    #[test]
    #[should_panic(expected = "message")]
    fn it_works() {
        // ...
    }
}
复制代码

在测试中使用 Result<T, E>

无需 panic,可使用 Result<T, E> 作为返回类型编写测试,返回 Ok 测试通过,返回 Err 测试失败。

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("err"))
        }
    }
}
复制代码

控制测试如何运行

添加参数可以改变 cargo test 的行为。

其默认行为是:并行运行所有测试,捕获(不显示)所有输出,使读取与测试结果相关的输出更容易。

命令行参数:

# 显示能跟哪些命令行参数
cargo test --help
# 显示可以用在 -- 之后的命令行参数
cargo test -- --help
#一个线程测试
cargo test -- --test-threads=1
复制代码

并行运行测试的时候虽然运行快,但是要注意确保测试之间不会相互依赖,不依赖某个共同状态(环境、工作目录、环境变量等)。

显示函数输出:默认如果测试通过,Rust 的 test 库会捕获所有打印到标准输出的内容。例如:如果被测试代码中用到了 println!,测试通过就不会在终端看到 println! 打印的内容,测试失败就可以看到这些信息。

如果想在成功的测试用例中看到打印的内容就需要加入 --show-output

按名称运行测试

将测试名称作为 cargo test 的参数选择运行的测试:

cargo test fn_name
复制代码

将测试名称的一部分(或者模块名)作为 cargo test 的参数选择运行的测试,可以就可以匹配多个测试:

cargo test part_of_fn_name
复制代码

忽略测试

忽略某些测试,运行剩余测试,可以使用 ignore 属性来标记。

#[cfg(test)]
mod tests {
    #[test]
    #[ignore]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 5);
    }
}
复制代码

如果要单独运行被忽略的测试可以使用:

cargo test -- --ignored
复制代码

测试的组织

Rust 将测试分为单元测试和集成测试两类。

单元测试:小、专注,一次对一个模块进行隔离测试,可以测试 private 接口。

集成测试:在库外部。和其它外部代码一样使用你的代码,只能使用 public 接口。可能在每个测试中使用到多个模块。

单元测试

tests 模块上的 #[cfg(test)] 就是对单元测试的标注。

标注之后的代码只有运行 cargo test 才编译和运行代码,运行 cargo build 则不会。

测试私有函数:

pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
}

// 私有函数
fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn it_works() {
        assert_eq!(4, internal_adder(2, 2));
    }
}
复制代码

集成测试

目的是测试被测试库的多个部分是否能正确的一起工作。集成测试的覆盖率很重要。

创建集成测试目录:tests,这个目录会被特殊对待,它与 src 并列。

tests 目录下的每个测试文件都是单独的一个 crate

// adder/tests/integration_test.rs
use adder; // 导入被测试库

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}
复制代码

运行

cargo test
复制代码

运行指定的集成测试

cargo test 函数名

# 或

cargo test --test 文件名
复制代码

如果现在集成测试目录下搞一些共享的代码,需要把他们写到 tests/common 目录下,否则会被当成测试代码直接执行。

针对 binary crate 的集成测试有一些不同,如果项目是 binary crate,只含有 src/main.rs 没有 src/lib.rs 就不能在 tests 目录下创建集成测试。即使有测试文件也无法把 main.rs 的函数导入作用域,因为只有 library crate 才能暴露函数给其它 crate 用。

binary crate 意味着要独立运行。

猜你喜欢

转载自juejin.im/post/7037043782948225055