【跟小嘉学 Rust 编程】十一、编写自动化测试

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试

文章目录

  • 系列文章目录
    • @[TOC](文章目录)
  • 前言
  • 一、如何编写测试
    • 1.1、测试函数
    • 1.2、运行测试
    • 1.2.1、运行测试:测试成功
    • 1.2.2、运行测试:测试失败
    • 1.3、断言(assert)
      • 1.3.1、使用 assert! 宏检测测试结果
      • 1.3.2、使用 `assert_eq!` 和 `assert_ne!` 测试相等性
    • 1.4、添加自定义错误消息
    • 1.5、验证 panic
      • 1.5.1、should_panic 属性
      • 1.5.2、should_panic 的 expected 参数
    • 1.6、在测试中使用 `Result `
  • 二、控制测试的运行方式
    • 2.1、cargo test 默认行为
    • 2.2、帮助手册
    • 2.3、并行/串行测试
      • 2.3.1、--test-threads 参数
    • 2.4、显示函数输出
    • 2.5、按照名称运行测试的子集
    • 2.6、忽略测试
  • 三、组织测试
    • 3.1、测试分类
    • 3.2、单元测试
      • 3.2.1、#[cfg(test)] 标注
      • 3.2.2、测试私有函数
    • 3.3、集成测试
      • 3.3.1、集成测试介绍
      • 3.3.2、集成测试用例
      • 3.3.3、运行指定的集成测试
      • 3.3.4、针对 binary crate 的集成测试
  • 总结

前言

程序测试是一种非常有效方法来测试Bug的存在,但是对于显示它们的缺失是完全不够的。程序的正确性是指代码在多大程度上完成了我们想让它做的事情。Rust 在设计时高度关注程序的正确性,但正确性时复杂的,而且不容易证明。本章节将会讲解测试函数、单元测试、集成测试、性能测试等等。

主要教材参考 《The Rust Programming Language》


一、如何编写测试

1.1、测试函数

测试是 Rust 函数,用于验证非测试代码是否按照预期方式运行。测试函数的主体通常执行以下三个动作(3A)。

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

要将函数改为测试,需要 fn 之前添加 #[test],当你是使用 cargo test 命令运行测试时, Rust 会构建一个测试运行二进制文件,该文件运行带注释的函数,并报告每个测试函数是通过还是失败。

当我们使用 Cargo 创建一个库的项目时,系统都会自动为我们生成一个包含测试函数的测试模块。本模块为您提供了编写测试的模板,因此您不必每次开始新项目时查找确切的结构和语法。

在实际测试任何代码之前,我们将通过模板测试来探索测试如何工作的一些方面。

$ cargo new adder --lib
     Created library `adder` project
$ cd adder

简单地来说,Rust中的测试时一个带有 test 属性(attribute)注释的函数。属性是关于 Rust 代码片段的元数据。

范例:测试函数

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

您可以添加任意数量的 test module 或 函数

1.2、运行测试

1.2.1、运行测试:测试成功

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

raojiamin@192 adder % cargo test
   Compiling adder v0.1.0 (~/Desktop/code/rust_code/adder)
    Finished test [unoptimized + debuginfo] target(s) in 1.98s
     Running unittests src/lib.rs (target/debug/deps/adder-b51bc6501e028464)

running 1 test
test tests::it_works ... 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 
  • Doc-Tests: 文档测试,用于保证文档和实际代码一致
  • test result(测试结果):
    • passed(通过数)
    • failed(失败数)
    • ignored(忽略数)
    • measured(性能测试)
    • filtered(过滤)

1.2.2、运行测试:测试失败

测试函数 panc 就表示失败,每个测试运行在一个新线程,当主线程看见某个线程挂掉了,那个测试标记为失败了。

范例代码:

pub fn add(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }

    #[test]
    fn another(){
        panic!("Make this test fail");
    }
}

运行结果

  Compiling adder v0.1.0 (~/Desktop/code/rust_code/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.84s
     Running unittests src/lib.rs (target/debug/deps/adder-b51bc6501e028464)

running 2 tests
test tests::it_works ... ok
test tests::another ... FAILED

failures:

---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:17:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

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

error: test failed, to rerun pass `--lib`

1.3、断言(assert)

1.3.1、使用 assert! 宏检测测试结果

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

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

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

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

        assert!(larger.can_hold(&smaller));
    }
}

1.3.2、使用 assert_eq!assert_ne! 测试相等性

都来自标准库,判断两个参数是否相等或不等,实际上,断言失败的时候,会自动打印出两个参数的值,使用 debug 格式打印参数,所以要求参数实现了 PartialEq 和 Debug Trait(所有的基本类型和标准库大部分类型都实现了)

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

    #[test]
    fn it_works() {
        let result = 2 + 2;
        assert_eq!(result, 4);
    }   
}

1.4、添加自定义错误消息

可以向 assert!、assert_eq!、 assert_ne! 可以添加可选的自定义信息。

  • 这些自定义消息和失败消息都会打印出来;
  • assert! :第一个参数必填,自定义消息为第二个参数
  • assert_eq! :前两个参数必填,自定义消息为第三个参数
  • assert_ne! :前两个参数必填,自定义消息为第三个参数

自定义消息会被传递给 format! 宏,可以使用 {} 占位符。

pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[test]
fn greeting_contains_name() {
     let result = greeting("Carol");
     assert!(
         result.contains("Carol"),
         "Greeting did not contain name, value was `{}`",
         result
     );
 }

1.5、验证 panic

1.5.1、should_panic 属性

测试出了验证代码的返回值是否正确,还需要验证代码是否如预期的处理发生了错误的情况。可验证代码在特定情况是否发生了 panic,should_panic 属性(attribute),函数如果发生了 panic,则测试通过,否则测试失败。

pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}

1.5.2、should_panic 的 expected 参数

在 should_panic 的属性中有一个参数 expected,可以用于测试期望的发生恐慌时的错误消息是否符合预期。

// --snip--

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }
        Guess { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    #[should_panic(expected = "less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

1.6、在测试中使用 Result

无需 panic,可使用 Result 作为返回类型编写测试:

  • 返回 Ok:测试通过
  • 返回 Err:测试失败
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    } 
}

二、控制测试的运行方式

2.1、cargo test 默认行为

  • 改变 cargoo test 的行为:添加命令行参数
  • 默认行为:
    • 并行运行;
    • 所有测试;
    • 捕获(不显示)所有输出,使读取与测试结果相关的输出更容易;
  • 命令行参数
    • 针对 cargoo test 的参数:紧跟 cargoo test
    • 针对测试可执行程序:放在 – 之后

2.2、帮助手册

cargo test --help
cargo test -- --help

2.3、并行/串行测试

运行多个测试,默认使用多线程并行运行,运行快。确保测试之间不会相互依赖,不依赖于某个共享状态(环境、工作目录、环境变量等等)。

2.3.1、–test-threads 参数

  • 传递给二进制文件
  • 不想以并行方式运行测试,或想对线程数进行细粒度控制;
  • 可以使用 --test-threads 参数,后边跟着线程的数量

例如 cargo test -- --test-threads=1

2.4、显示函数输出

默认,如测试通过,Rust 的 test 库会补货所有打印到标准输出的内容。

例如:如果被测试代码中用到了 println!

  • 如果测试通过,不会在终端看到 println! 打印的内容;
  • 如果测试失败,会看到 println! 打印的内容 和 失败信息;

如果想在成功的测试中看到打印的内容:--show-output

2.5、按照名称运行测试的子集

选择运行的测试:将测试的名称(一个或多个)作为 cargo test 的参数

  • 运行单个测试:指定测试名;
  • 运行多个测试:指定测试名的一部分(模块名也可以)

2.6、忽略测试

忽略某些测试,运行剩余测试

  • ignore 属性(attribute)
  • 运行被忽略(ignore)的测试: cargo test -- --ignored

三、组织测试

3.1、测试分类

Rust 对测试的分类:

  • 单元测试:
    • 小、专注
    • 一次对一个模块进行隔离的测试
    • 可测试 private 接口
  • 集成测试:
    • 在库外部,和其他外部代码一样使用你的代码
    • 只能使用 public 接口
    • 可能在每个测试中使用到多个模块

3.2、单元测试

3.2.1、#[cfg(test)] 标注

在 tests 模块上的 #[cfg(test)] 标注 ,只有在运行 cargo test 才会编译和运行代码,运行 cargo build 则不会

集成测试在不同的目录,它不需要 #[cfg(test)] 标注

cfg:configuration(配置)

  • 告诉 Rust 下面的条目只有在指定配置选项下才会被包含
  • 配置选项 test: 由 Rust 提供,用来编译和运行测试
    • 只有 cargo test 才会编译代码,包括模块中的 helper 函数 和 #[test] 标注的函数

3.2.2、测试私有函数

在Rust 中允许测试私有函数

3.3、集成测试

3.3.1、集成测试介绍

在 Rust 里面,集成测试完全位于被测试库的外部,目的是为了测试被测试库的多个部分是否能正确的一起工作。

  • 集成测试覆盖率是一个很重要的指标。
  • 集成测试目录:tests 目录,tests 目录下的每个测试文件都是单独的一个 crate,需要将被测试库导入。
  • 无需标注 #[cfg(test)] ,tests 目录被特殊对待,只有 cargo test,才会编译 tests 目录下的文件;

3.3.2、集成测试用例

3.3.3、运行指定的集成测试

  • 运行一个特定的集成测试:cargo test 函数名
  • 运行某个测试文件内的所有测试:cargo test --test 文件名

3.3.4、针对 binary crate 的集成测试

  • 如果项目是 binary crate ,只含有 src/main.rs,没有 src/lib.rs:
    • 不能在 tests 目录下创建集成测试
    • 无法把 main.rs 的函数导入作用域
  • 只有 library crate 才能暴露函数给其他 crate 使用
  • binary crate 意味着独立运行

总结

以上就是今天要讲的内容

  • 如何编写测试文件
  • 如何运行测试文件
  • 如何编写单元测试和集成测试

你可能感兴趣的:(跟小嘉学,Rust,编程,rust,开发语言,后端)