Rust

2025年07月14日更新 7 人订阅
原价: ¥ 6 限时优惠
专栏简介 Rust编程语言之错误处理 Rust 语言之 flod Rust编程语言之Cargo、Crates.io详解 Rust编程语言之枚举与模式匹配 Rust语言 - 接口设计的建议之受约束(Constrained) Rust编程语言之无畏并发 Rust语言 - 接口设计的建议之灵活(flexible) Rust语言 - 接口设计的建议之显而易见(Obvious) Rust语言 - 接口设计的建议之不意外(unsurprising) Rust 实战:构建实用的 CLI 工具 HTTPie Rust编程语言学习之高级特性 Rust内存管理揭秘:深度剖析指针与智能指针 解决Rust中数组和切片的编译时大小问题 《Rust编程之道》学习笔记一 Rust Async 异步编程 简易教程 使用 Async Rust 构建简单的 P2P 节点 Rust编程语言入门之模式匹配 Rust async 编程 Rust编程语言之编写自动化测试 Rust编程语言之函数式语言特性:迭代器和闭包 《Rust编程之道》学习笔记二 Rust Tips 比较数值 使用 Rust 开发一个微型游戏 Rust编程初探:深入理解Struct结构体 深入理解Rust中的内存管理:栈、堆与静态内存详解 深入理解 Rust 结构体:经典结构体、元组结构体和单元结构体的实现 深入掌握 Rust 结构体:从模板到实例化的完整指南 深入理解Rust中的结构体:逻辑与数据结合的实战示例 深入理解 Rust 枚举:从基础到实践 掌握Rust字符串的精髓:String与&str的最佳实践 全面解析 Rust 模块系统:实战案例与应用技巧 Rust 中的 HashMap 实战指南:理解与优化技巧 掌握Rust模式匹配:从基础语法到实际应用 Rust 中的面向对象编程:特性与实现指南 深入理解 Rust 的 Pin 和 Unpin:理论与实践解析 Rust Trait 与 Go Interface:从设计到实战的深度对比 从零开始:用 Rust 和 Axum 打造高效 Web 应用 Rust 错误处理详解:掌握 anyhow、thiserror 和 snafu Rust 如何优雅实现冒泡排序 链表倒数 K 节点怎么删?Python/Go/Rust 实战 用 Rust 玩转数据存储:JSON 文件持久化实战 Rust实战:打造高效字符串分割函数 如何高效学习一门技术:从知到行的飞轮效应 Rust 编程入门:Struct 让代码更优雅 Rust 编程:零基础入门高性能开发 用 Rust 写个猜数游戏,编程小白也能上手! Rust 入门教程:变量到数据类型,轻松掌握! 深入浅出 Rust:函数、控制流与所有权核心特性解析 从零开始:用 Rust 和 Axum 打造高效 Web 服务 Rust 集合类型解析:Vector、String、HashMap 深入浅出Rust:泛型、Trait与生命周期的硬核指南 Rust实战:博物馆门票限流系统设计与实现 用 Rust 打造高性能图片处理服务器:从零开始实现类似 Thumbor 的功能 Rust 编程入门实战:从零开始抓取网页并转换为 Markdown 深入浅出 Rust:高效处理二进制数据的 Bytes 与 BytesMut 实战 Rust智能指针:解锁内存管理的进阶之道 用 Rust 打造命令行利器:从零到一实现 mini-grep 解锁Rust代码组织:轻松掌握Package、Crate与Module Rust 所有权:从内存管理到生产力释放 深入解析 Rust 的面向对象编程:特性、实现与设计模式 Rust + Protobuf:从零打造高效键值存储项目 bacon 点燃 Rust:比 cargo-watch 更爽的开发体验 用 Rust 打造微型游戏:从零开始的 Flappy Dragon 开发之旅 函数式编程的Rust之旅:闭包与迭代器的深入解析与实践 探索Rust编程之道:从设计哲学到内存安全的学习笔记 精读《Rust编程之道》:吃透语言精要,彻底搞懂所有权与借用 Rust 避坑指南:搞定数值比较,别再让 0.1 + 0.2 != 0.3 困扰你! 告别 Vec!掌握 Rust bytes 库,解锁零拷贝的真正威力 告别竞态条件:基于 Axum 和 Serde 的 Rust 并发状态管理最佳实践 Rust 异步编程实践:从 Tokio 基础到阻塞任务处理模式 Rust 网络编程实战:用 Tokio 手写一个迷你 TCP 反向代理 (minginx) 保姆级教程:Zsh + Oh My Zsh 终极配置,让你的 Ubuntu 终端效率倍增 不止于后端:Rust 在 Web 开发中的崛起之路 (2024数据解读) Rust核心利器:枚举(Enum)与模式匹配(Match),告别空指针,写出优雅健壮的代码 Rust 错误处理终极指南:从 panic! 到 Result 的优雅之道 想用 Rust 开发游戏?这份超详细的入门教程请收好! 用 Rust 实现 HTTPie:一个现代 CLI 工具的构建过程 Rust 异步实战:从0到1,用 Tokio 打造一个高性能并发聊天室

Rust编程语言之编写自动化测试

Rust编写自动化测试一、编写和运行测试测试(函数)测试:函数验证非测试代码的功能是否和预期一致测试函数体(通常)执行的3个操作:准备数据/状态运行被测试的代码断言(Assert)结果解剖测试函数测试函数需要使用test属性(attribute)进行标注

Rust编写自动化测试

一、编写和运行测试

测试(函数)

  • 测试:
    • 函数
    • 验证非测试代码的功能是否和预期一致
  • 测试函数体(通常)执行的3个操作:
    • 准备数据/状态
    • 运行被测试的代码
    • 断言(Assert)结果

解剖测试函数

  • 测试函数需要使用 test 属性(attribute)进行标注
    • Attribute就是一段Rust代码的元数据
    • 在函数上加 #[test],可把函数变成测试函数

运行测试

  • 使用 cargo test 命令运行所有测试函数

    • Rust会构建一个 Test Runner 可执行文件
    • 它会运行标注了 test 的函数,并报告其运行是否成功
  • 当使用 cargo 创建 library 项目的时候,会生成一个 test module,里面有一个test 函数

    • 你可以添加任意数量的 test module 或 函数
~/rust
➜ cargo new adder --lib
     Created library `adder` package

~/rust
➜ cd adder

adder on  master [?] via 🦀 1.67.1
➜ code .

adder on  master [?] via 🦀 1.67.1 took 2.2s
➜

~/rust
➜ cargo new adder --lib
     Created library `adder` package

~/rust
➜ cd adder

adder on  master [?] via 🦀 1.67.1
➜ code .

adder on  master [?] via 🦀 1.67.1 took 2.2s
➜

lib.rs 文件

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);
    }
}

测试失败

  • 测试函数 panic 就表示失败
  • 每个测试运行在一个新线程
  • 当主线程看见某个测试线程挂掉了,那个测试标记为失败了。
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

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

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

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

运行

adder on  master [?] is 📦 0.1.0 via 🦀 1.67.1 took 3.0s 
➜ cargo test
   Compiling adder v0.1.0 (/Users/qiaopengjun/rust/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.16s
     Running unittests src/lib.rs (target/debug/deps/adder-6058f7b13179a51e)

running 2 tests
test tests::exploration ... 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`

adder on  master [?] is 📦 0.1.0 via 🦀 1.67.1 

二、断言(Assert)

使用 Assert! 宏检查测试结果

  • assert! 宏,来自标准库,用来确定某个状态是否为true
    • true:测试通过
    • false:调用 panic!,测试失败
#[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));
  }

  #[test]
  fn samller_cannot_hold_larger() {
    let larger = Rectangle {
      length: 8,
      width: 7,
    };
    let smaller = Rectangle {
      length: 5,
      width: 1,
    };
    assert!(!smaller.can_hold(&larger));
  }
}

使用 assert_eq! 和 assert_ne! 测试相等性

  • 都来自标准库
  • 判断两个参数是否相等或不等
  • 实际上,它们使用的就是 == 和 !== 运算符
  • 断言失败,自动打印出两个参数的值
    • 使用 debug 格式打印参数
    • 要求参数实现了 PartialEq 和 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_ne!(5, add_two(2));
  }
}

三、自定义错误消息

添加自定义错误信息

  • 可以向 assert!、assert_eq!、assert_ne! 添加可选的自定义消息
    • 这些自定义消息和失败消息都会打印出来
    • assert!:第 1 参数必填,自定义消息作为第2个参数。
    • assert_eq! 和 assert_ne!:前2个参数必填,自定义消息作为第 3 个参数。
    • 自定义消息参数会被传递给 format! 宏,可以使用 {} 占位符
pub fn greeting(name: &str) -> String {
  //format!("Hello {}!", name)
  format!("Hello!")
}

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

  #[test]
  fn greetings_contain_name() {
    let result = greeting("Carol");
    // assert!(result.contains("Carol"));
    assert!(
      result.contains("Carol"),
      "Greeting didn't contain name, value was '{}'", result
    );
  }
}

四、用 should_panic 检查恐慌

验证错误处理的情况

  • 测试除了验证代码的返回值是否正确,还需验证代码是否如预期的处理了发生错误的情况
  • 可验证代码在特定情况下是否发生了 panic
  • should_panic 属性(attribute):
    • 函数 panic:测试通过
    • 函数 没有 panic:测试失败
pub struct Guess {
  value: u32,
}

impl Guess {
  pub fn new(value: u32) -> 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: u32,
}

impl Guess {
  pub fn new(value: u32) -> 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 = "Guess value must be less than or equal to 100")]
  fn greater_than_100() {
    Guess::new(200);
  }
}

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

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

  • 无需 panic,可使用 Result<T, E> 作为返回类型编写测试:
    • 返回 Ok:测试通过
    • 返回 Err:测试失败
#[cfg(test)]
mod tests {
  #[test]
  fn it_works() -> Result&lt;(), String> {
    if 2 + 2 == 4 {
      Ok(())
    } else {
      Err(String::from("two plus two does not equal four"))
    }
  }
}
  • 注意:不要在使用 Result<T, E> 编写的测试上标注 #[should_panic]

六、控制测试运行:并行和连续执行测试

控制测试如何运行

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

并行运行测试

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

--test-threads 参数

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

显示函数输出

  • 默认,如测试通过,Rust的test库会捕获所有打印到标准输出的内容
  • 例如,如果被测试代码中用到了 println!:
    • 如果测试通过:不会再终端看到 println! 打印的内容
    • 如果测试失败:会看到 println! 打印的内容和失败信息
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);
  }
}
  • 如果想在成功的测试中看到打印的内容: --show-output

七、控制测试运行:按名称运行测试

  • 选择运行的测试:将测试的名称(一个或多个)作为 cargo test 的参数
pub fn add_two(a: i32) -> i32 {
  a + 2
}

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

  #[test]
  fn add_two_and_two() {
    assert_eq!(4, add_two(2));
  }

  #[test]
  fn add_three_and_two() {
    assert_eq!(5, add_two(3));
  }

  #[test]
  fn one_hundred() {
    assert_eq!(102, add_two(100));
  }
}
  • 运行单个测试:指定测试名
cargo test one_hundred
  • 运行多个测试:指定测试名的一部分(模块名也可以)
  • cargo test add

八、控制测试运行:忽略测试

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

  • ignore 属性(attribute)
#[cfg(test)]
mod tests {
  #[test]
  fn it_works() {
    assert_eq!(4, 2 + 2);
  }

  #[test]
  #[ignore]
  fn expensive_test() {
    assert_eq!(5, 1 + 1 + 1 + 1 + 1);
  }
}
  • 运行被忽略(ignore)的测试:
    • cargo test -- --ignored

九、测试组织:单元测试

测试的分类

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

单元测试

[cfg(test)] 标注

  • tests 模块上的 #[cfg(test)] 标注:
    • 只有运行 cargo test 才编译和运行代码
    • 运行 cargo build 则不会
  • 集成测试在不同的目录,它不需要 #[cfg(test)] 标注
  • cfg:configuration (配置)
    • 告诉Rust下面的条目只有在指定的配置选项下才被包含
    • 配置选项test:由Rust提供,用来编译和运行测试
    • 只有 cargo test 才会编译代码,包括模块中的 helper 函数 和 #[test] 标注的函数
#[cfg(test)]
mod tests {
  #[test]
  fn it_works() {
    assert_eq!(4, 2 + 2);
  }
}

测试私有函数

  • Rust允许测试私有函数
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));
  }
}

十、集成测试

  • 在Rust里,集成测试完全位于被测试库的外部
  • 目的:是测试被测试库的多个部分是否能正确的一起工作
  • 集成测试的覆盖率很重要

Tests 目录

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

src/lib.rs 文件

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

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

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

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

tests/integration_test.rs 文件

use adder;

#[test]
fn it_add() {
    assert_eq!(5, adder::add(2, 3));
}

运行

adder on  master [?] is 📦 0.1.0 via 🦀 1.67.1 
➜ cargo test
   Compiling adder v0.1.0 (/Users/qiaopengjun/rust/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.11s
     Running unittests src/lib.rs (target/debug/deps/adder-6058f7b13179a51e)

running 1 test
test tests::exploration ... ok

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

     Running tests/integration_test.rs (target/debug/deps/integration_test-461b916f2718e782)

running 1 test
test it_add ... 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

adder on  master [?] is 📦 0.1.0 via 🦀 1.67.1 

运行指定的集成测试

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

tests/another_integration_tests.rs 文件

use adder;

#[test]
fn it_adds2() {
    assert_eq!(7, adder::add(3,4));
}

运行

adder on  master [?] is 📦 0.1.0 via 🦀 1.67.1 
➜ cargo test --test integration_test
    Finished test [unoptimized + debuginfo] target(s) in 0.00s
     Running tests/integration_test.rs (target/debug/deps/integration_test-461b916f2718e782)

running 1 test
test it_add ... ok

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

adder on  master [?] is 📦 0.1.0 via 🦀 1.67.1 
➜ cargo test --test another_integration_tests
   Compiling adder v0.1.0 (/Users/qiaopengjun/rust/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.11s
     Running tests/another_integration_tests.rs (target/debug/deps/another_integration_tests-0a89cbf68d5b375f)

running 1 test
test it_adds2 ... ok

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

adder on  master [?] is 📦 0.1.0 via 🦀 1.67.1 

集成测试中的子模块

  • tests 目录下每个文件被编译成单独的 crate
    • 这些文件不共享行为(与 src 下的文件规则不同)
adder on  master [?] is 📦 0.1.0 via 🦀 1.67.1 
➜ tree
.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
├── target
│   ├── CACHEDIR.TAG
│   ├── debug
│   └── tmp
└── tests
    ├── another_integration_tests.rs
    ├── common
    │   └── mod.rs
    └── integration_test.rs

27 directories, 205 files

tests/common/mod.rs 文件

pub fn setup() {}

tests/another_integration_tests.rs 文件

use adder;

mod common;

#[test]
fn it_adds2() {
    common::setup();
    assert_eq!(7, adder::add(3,4));
}

tests/integration_test.rs 文件

use adder;

mod common;

#[test]
fn it_add() {
    common::setup();
    assert_eq!(5, adder::add(2, 3));
}

针对 binary crate 的集成测试

  • 如果项目是 binary Crate,只含有 src/main.rs 没有 src/lib.rs:
    • 不能在 tests 目录下创建集成测试
    • 无法把 main.rs 的函数导入作用域
  • 只有library crate 才能暴露函数给其它 crate 用
  • binary crate 意味着独立运行
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论