Rust(十一)-编写自动化测试

361 阅读6分钟

测试(函数)

  • 测试:
    • 函数
    • 验证非测试代码的功能是否和预期一样
  • 测试函数体(通常)执行的3个操作:
    • 准备数据/状态
    • 运行被测试的代码
    • 断言(assert)结果
  • 解剖测试函数
    • 测试函数需要使用test属性(attribute)进行标注
      • attribute 就是一段Rust代码的元数据
      • 在函数加#[test],可把函数变成测试函数
  • 运行测试
    • 使用cargo test命令运行所有测试函数
      • rust会构奸一个test runner可执行文件
        • 它会运行标注了test的函数,并报告其运行是否成功
    • 当使用cargo创建library项目的时候,会生成一个test module,里面有一个test函数
      • 你可以添加任意数量的test Module 或 函数
      #[cfg(test)]
         mod tests {
             #[test]
             fn it_works() {
                 let result = 2 + 2;
                 assert_eq!(result, 4);
             }
         }
      

image.png

  • 测试失败
    • 测试函数panic就表示失败
    • 每个测试运行在一个新线程
    • 当主线程看见某个测试线程挂掉了,那个测试标记为失败了
    #[cfg(test)]
      mod tests {
          #[test]
          fn exploration() {
              assert_eq!(2 + 2, 4);
          }
    
          #[test]
          fn another() {
              panic!("Make this test fail");
          }
      }
    

断言(assert)

  • 使用assert!宏检查测试结果
    • assert!宏, 来自标准库, 用来确定某个状态是否为true
      • true: 测试通过
      • False: 调用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));
              }
          }
      
    • 使用assert_eq! 和 assert_ne!测试相等性
      • 都来自标准库
      • 判断两个参数是否相等或不等
      • 实际上,它们使用的就是 == 和 != 运算符
      • 断言失败:自动打印出两个参数的值
        • 使用debug格式打印参数的值
          • 使用debug格式打印参数
            • 要求参数实现了partialep和debug traits (所有的基本类型和标准库里大部分类型都实现了)
      pub fn add_two(a: i32) -> i32 {
          a + 2
      }
      
      #[cfg(test)]
      mod tests {
          use super::*;
      
          #[test]
          fn it_adds_two() {
              assert_eq!(4, add_two(2));
          }
      }
      
  • 自定义错误消息
    • 可以向assert!、assert_eq!、assert_ne!添加可选的自定义消息
      • 这些自定义消息和失败消息都会打印出来
      • assert!:第1参数必填,自定义消息作为第2个参数。
      • assert_eq!和 assert_ne!: 前两个参数必填,自定义消息作为第3个参数
      • 自定义消息参数会被传递给format宏, 可以使用{}占位符
      pub fn greeting(name: &str) -> String {
          String::from("Hello!")
      }
      
      #[cfg(test)]
      mod tests {
          use super::*;
      
          #[test]
          fn greeting_contains_name() {
              let result = greeting("Carol");
              assert!(
                  result.contains("Carol"),
                  "Greeting did not contain name, value was `{}`",
                  result
              );
          }
      }
      
      
  • 用 should_panic检查恐慌
    • 验证错误处理的情况
      • 测试除了验证代码的返回值是否正确,还需验证代码是否如预期的处理了发生错误的情况
      • 可验证代码是特定情况下是否发生了panic
      • should_panic属性(attribute):
        • 函数panic: 测试通过
        • 函数没有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);
              }
          }
        
    • 让should_panic更精确
    • 为should_panic属性添加一个可选的expected参数:
      • 将检查失败消息中是否包含所指定的文字
      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);
            }
        }
      
  • 在测试中使用result<T,E>
    • 无需panic,可使用Result<T,E>作为返回类型编写测试:
      • 返回ok:测试通过
      • 返回err: 测试失败
    • 注意:不要在使用Result<T,E> 编写的测试上标注#[should_panic]

控制测试如何运行

  • 改变cargo test的行为:添加命令行参数
  • 默认行为
    • 并行运行
    • 所有测试
    • 捕获(不显示)所有输出, 使读取与测试结果相关的输出更容易
  • 命令行参数:
    • 针对cargo test的参数:紧跟cargo test后
    • 针对测试可执行程序:放在-- 之后
  • cargo test --help
  • cargo test -- --htlp
  • 并行运行测试
    • 运动多个测试:默认使用多个线程并行运行
      • 运行快
    • 确保测试之间:
      • 不会互相依赖
      • 不依赖于某个共享状态(环境、工作目录、环境变量等等)
    • 控制线程的数量
      • 传递给二进制文件
      • 不想以并行方式运行测试,或想对线程数进行细粒度控制
      • 可以使用 --test-threads参数,后边跟着线程的数量
      • 例如:cargo test -- -- test-threads=1
  • 显式函数输出
    • 默认,如测试通过,rust的test库会捕获所有打印到标准输出的内容
    • 例如:如果被测试代码中用到了println!
      • 如果测试通过:不会在终端看到println!打印的内容
      • 如果测试失败:会看到Println!打印的内容和失败信息
      • 如果想在成功的测试中看到打印的内容: -- show-output
      fn prints_and_returns_10(a: i32) -> i32 {
            println!("I got the value {}", a);
            10
        }
      
        #[cfg(test)]
        mod tests {
            use super::*;
      
            #[test]
            fn this_test_will_pass() {
                let value = prints_and_returns_10(4);
                assert_eq!(10, value);
            }
      
            #[test]
            fn this_test_will_fail() {
                let value = prints_and_returns_10(8);
                assert_eq!(5, value);
            }
        }
      

按名称运行测试

  • 按名称运行测试的子集
    • 选择运行的测试:将测试的名称(一个或多个)作为cargo test 的参数
    • cargo test test_name test_name1 (参数只传一个)
    • 运行单个测试:指定测试名
    • 运行多个测试: 指定测试名的一部分(模块名也可以)
  • 忽略测试,运行其它测试
    • ignore(属性) atrribute
    #[test]
     fn it_works() {
         assert_eq!(2 + 2, 4);
     }
    
     #[test]
     #[ignore]
     fn expensive_test() {
         // code that takes an hour to run
     }
    
    • 运行被忽略的测试:
      • cargo test -- --ignored
  • 测试的分类
    • 单元测试
      • 小、专注
      • 一次对一个模块进行隔离的测试
      • 可测试private接口
      #[cfg(test)]
          mod tests {
              #[test]
              fn it_works() {
                  let result = 2 + 2;
                  assert_eq!(result, 4);
              }
          }
      
      #[cfg(test)]标注
      • test 模块上的 #[cfg(test)] 标注:
        • 只有运行cargo test才编译和运行代码
        • 运行 cargo Build则不会
    • 集成测试
      • 在库外部,和其它外部代码一样使用你的代码
      • 只能使用public接口
      • 可能在每个测试中使用到多个模块
      • 集成测试在不同的目录,它不需要 #[cfg(test)] 标注
  • cfg: configuratin(配置)
    • 告诉rust下面的条目只有在指定的配置选项下才被包含
    • 配置选项test:由Rust提供,用来编译和运行测试
      • 只有cargo test 才会编译代码,包括模块中的helper函数和#[test]标注的函数
  • 集成测试
    • 在Rust里,集成测试完全位于被测试库的外部
    • 目的:是测试被测试库的多个部分是否能正确的一起工作
    • 集成测试的覆盖率很重要
  • tests 目录
    • 创建集成测试: tests目录
    • test目录下的每个测试文件都是单独的一个crate
    • cargo test
    • 无需标注 #[cfg(test)],tests目录被特殊对待
      • 只有cargo test,才会编译tests目录下的文件
  • 运行指定的集成测试
    • 运行一个特定的集成测试: cargo test函数名
    • 运行某个测试文件内的所有测试: cargo test --test 文件名
  • 针对 binary crate的集成测试
    • 如果项目是binary crate,只含有src/main.rs 没有Src/lib.rs
      • 不能在tests目录下创建集成测试

      • 无法把main.rs的函数导入作用域