Testing Patterns for C++ Programmers / C++ 程序员的测试模式
What you’ll learn / 你将学到: Rust’s built-in test framework —
#[test],#[should_panic],Result-returning tests, builder patterns for test data, trait-based mocking, property testing withproptest, snapshot testing withinsta, and integration test organization. Zero-config testing that replaces Google Test + CMake.Rust 内置的测试框架 ——
#[test]、#[should_panic]、返回Result的测试、测试数据的构建器模式、基于 trait 的模拟(mocking)、使用proptest进行属性测试、使用insta进行快照测试,以及集成测试的组织。这种零配置测试将取代 Google Test + CMake。
- C++ testing typically relies on external frameworks (Google Test, Catch2, Boost.Test)
- C++ 测试通常依赖于外部框架(Google Test、Catch2、Boost.Test)
- with complex build integration. Rust’s test framework is **built into the language
- 并伴随着复杂的构建集成。Rust 的测试框架是内置于语言和工具链中的 —— 无需依赖,无需 CMake 集成,也无需配置测试运行器(test runner)。
-
and toolchain** — no dependencies, no CMake integration, no test runner configuration.
-
Test attributes beyond
#[test]
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_pass() {
assert_eq!(2 + 2, 4);
}
- // Expect a panic — equivalent to GTest's EXPECT_DEATH
+ // Expect a panic — equivalent to GTest's EXPECT_DEATH / 预期发生 panic —— 相当于 GTest 的 EXPECT_DEATH
#[test]
#[should_panic]
fn out_of_bounds_panics() {
let v = vec![1, 2, 3];
- let _ = v[10]; // Panics — test passes
+ let _ = v[10]; // Panics — test passes / 发生 panic —— 测试通过
}
- // Expect a panic with a specific message substring
+ // Expect a panic with a specific message substring / 预期带有特定消息子串的 panic
#[test]
#[should_panic(expected = "index out of bounds")]
fn specific_panic_message() {
let v = vec![1, 2, 3];
let _ = v[10];
}
- // Tests that return Result<(), E> — use ? instead of unwrap()
+ // Tests that return Result<(), E> — use ? instead of unwrap() / 返回 Result<(), E> 的测试 —— 使用 ? 代替 unwrap()
#[test]
fn test_with_result() -> Result<(), String> {
let value: u32 = "42".parse().map_err(|e| format!("{e}"))?;
assert_eq!(value, 42);
Ok(())
}
- // Ignore slow tests by default — run with `cargo test -- --ignored`
+ // Ignore slow tests by default — run with `cargo test -- --ignored` / 默认忽略慢速测试 —— 使用 `cargo test -- --ignored` 运行
#[test]
#[ignore]
fn slow_integration_test() {
std::thread::sleep(std::time::Duration::from_secs(10));
}
}
}
- cargo test # Run all non-ignored tests
+ cargo test # Run all non-ignored tests / 运行所有未被忽略的测试
- cargo test -- --ignored # Run only ignored tests
+ cargo test -- --ignored # Run only ignored tests / 仅运行被忽略的测试
- cargo test -- --include-ignored # Run ALL tests including ignored
+ cargo test -- --include-ignored # Run ALL tests including ignored / 运行包括忽略测试在内的所有测试
- cargo test test_name # Run tests matching a name pattern
+ cargo test test_name # Run tests matching a name pattern / 运行匹配名称模式的测试
- cargo test -- --nocapture # Show println! output during tests
+ cargo test -- --nocapture # Show println! output during tests / 测试期间显示 println! 输出
- cargo test -- --test-threads=1 # Run tests serially (for shared state)
+ cargo test -- --test-threads=1 # Run tests serially (for shared state) / 串行运行测试(用于共享状态)
- In C++ you’d use Google Test fixtures (
class MyTest : public ::testing::Test).
- 在 C++ 中,你会使用 Google Test 固件(Google Test fixtures,
class MyTest : public ::testing::Test)。
- In Rust, use builder functions or the
Defaulttrait:
- 在 Rust 中,使用构建器函数或
Defaulttrait:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
- // Builder function — creates test data with sensible defaults
+ // Builder function — creates test data with sensible defaults / 构建器函数 —— 使用合理的默认值创建测试数据
fn make_gpu_event(severity: Severity, fault_code: u32) -> DiagEvent {
DiagEvent {
source: "accel_diag".to_string(),
severity,
message: format!("Test event FC:{fault_code}"),
fault_code,
}
}
- // Reusable test fixture — a set of pre-built events
+ // Reusable test fixture — a set of pre-built events / 可重用的测试固件 —— 一组预构建的事件
fn sample_events() -> Vec<DiagEvent> {
vec![
make_gpu_event(Severity::Critical, 67956),
make_gpu_event(Severity::Warning, 32709),
make_gpu_event(Severity::Info, 10001),
]
}
#[test]
fn filter_critical_events() {
let events = sample_events();
let critical: Vec<_> = events.iter()
.filter(|e| e.severity == Severity::Critical)
.collect();
assert_eq!(critical.len(), 1);
assert_eq!(critical[0].fault_code, 67956);
}
}
}
- In C++, mocking requires frameworks like Google Mock or manual virtual overrides.
- 在 C++ 中,模拟(mocking)需要像 Google Mock 这样的框架或手动进行虚函数覆盖。
- In Rust, define a trait for the dependency and swap implementations in tests:
- 在 Rust 中,为依赖项定义一个 trait 并在测试中更换实现:
#![allow(unused)]
fn main() {
- // Production trait
+ // Production trait / 生产环境使用的 trait
trait SensorReader {
fn read_temperature(&self, sensor_id: u32) -> Result<f64, String>;
}
- // Production implementation
+ // Production implementation / 生产环境实现
struct HwSensorReader;
impl SensorReader for HwSensorReader {
fn read_temperature(&self, sensor_id: u32) -> Result<f64, String> {
- // Real hardware call...
+ // Real hardware call... / 真实的硬件调用...
Ok(72.5)
}
}
- // Test mock — returns predictable values
+ // Test mock — returns predictable values / 测试模拟 —— 返回可预测的值
#[cfg(test)]
struct MockSensorReader {
temperatures: std::collections::HashMap<u32, f64>,
}
#[cfg(test)]
impl SensorReader for MockSensorReader {
fn read_temperature(&self, sensor_id: u32) -> Result<f64, String> {
self.temperatures.get(&sensor_id)
.copied()
.ok_or_else(|| format!("Unknown sensor {sensor_id}"))
}
}
- // Function under test — generic over the reader
+ // Function under test — generic over the reader / 待测函数 —— 对 reader 进行泛型处理
fn check_overtemp(reader: &impl SensorReader, ids: &[u32], threshold: f64) -> Vec<u32> {
ids.iter()
.filter(|&&id| reader.read_temperature(id).unwrap_or(0.0) > threshold)
.copied()
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_overtemp_sensors() {
let mut mock = MockSensorReader { temperatures: Default::default() };
- mock.temperatures.insert(0, 72.5);
+ mock.temperatures.insert(0, 72.5); // 存入模拟值
- mock.temperatures.insert(1, 91.0); // Over threshold
+ mock.temperatures.insert(1, 91.0); // Over threshold / 超过阈值
mock.temperatures.insert(2, 65.0);
let hot = check_overtemp(&mock, &[0, 1, 2], 80.0);
assert_eq!(hot, vec![1]);
}
}
}
- C++ tests often use platform-specific temp directories. Rust has
tempfile:
- C++ 测试通常使用特定于平台的临时目录。Rust 有
tempfile:
#![allow(unused)]
fn main() {
// Cargo.toml: [dev-dependencies]
// tempfile = "3"
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
use std::io::Write;
#[test]
fn parse_config_from_file() -> Result<(), Box<dyn std::error::Error>> {
- // Create a temp file that's auto-deleted when dropped
+ // Create a temp file that's auto-deleted when dropped / 创建一个在释放(dropped)时自动删除的临时文件
let mut file = NamedTempFile::new()?;
writeln!(file, r#"{{"sku": "ServerNode", "level": "Quick"}}"#)?;
let config = load_config(file.path().to_str().unwrap())?;
assert_eq!(config.sku, "ServerNode");
Ok(())
- // file is deleted here — no cleanup code needed
+ // file is deleted here — no cleanup code needed / 文件在这里被删除 —— 无需清理代码
}
}
}
- Instead of writing specific test cases, describe properties that should hold
- 与其编写特定的测试用例,不如描述对于所有输入都应成立的属性(properties)。
- for all inputs.
proptestgenerates random inputs and finds minimal failing cases:
proptest会生成随机输入并找到最小的失败用例:
#![allow(unused)]
fn main() {
// Cargo.toml: [dev-dependencies]
// proptest = "1"
#[cfg(test)]
mod tests {
use proptest::prelude::*;
fn parse_and_format(n: u32) -> String {
format!("{n}")
}
proptest! {
#[test]
- fn roundtrip_u32(n: u32) {
+ fn roundtrip_u32(n: u32) { // 测试往返转换
let formatted = parse_and_format(n);
let parsed: u32 = formatted.parse().unwrap();
prop_assert_eq!(n, parsed);
}
#[test]
- fn string_contains_no_null(s in "[a-zA-Z0-9 ]{0,100}") {
+ fn string_contains_no_null(s in "[a-zA-Z0-9 ]{0,100}") { // 字符串不包含 null
prop_assert!(!s.contains('\0'));
}
}
}
}
- For tests that produce complex output (JSON, formatted strings),
instaauto-generates
- 对于产生复杂输出(JSON、格式化字符串)的测试,
insta会自动生成并管理参考快照:
- and manages reference snapshots:
#![allow(unused)]
fn main() {
// Cargo.toml: [dev-dependencies]
// insta = { version = "1", features = ["json"] }
#[cfg(test)]
mod tests {
use insta::assert_json_snapshot;
#[test]
fn der_entry_format() {
let entry = DerEntry {
fault_code: 67956,
component: "GPU".to_string(),
message: "ECC error detected".to_string(),
};
- // First run: creates a snapshot file in tests/snapshots/
+ // First run: creates a snapshot file in tests/snapshots/ / 第一次运行:在 tests/snapshots/ 中创建一个快照文件
- // Subsequent runs: compares against the saved snapshot
+ // Subsequent runs: compares against the saved snapshot / 后续运行:与保存的快照进行比较
assert_json_snapshot!(entry);
}
}
}
- cargo insta test # Run tests and review new/changed snapshots
+ cargo insta test # Run tests and review snapshots / 运行测试并查看新快照或变更的快照
- cargo insta review # Interactive review of snapshot changes
+ cargo insta review # Interactive review of snapshot changes / 以交互方式查看快照变更
-| C++ (Google Test) | Rust | Notes |
+| C++ (Google Test) | Rust | Notes / 说明 |
|–––––––––––|———|–––––|
-| TEST(Suite, Name) { } | #[test] fn name() { } | No suite/class hierarchy needed |
+| TEST(Suite, Name) { } | #[test] fn name() { } | No suite/class hierarchy needed / 无需测试套件或类层次结构 |
-| ASSERT_EQ(a, b) | assert_eq!(a, b) | Built-in macro, no framework needed |
+| ASSERT_EQ(a, b) | assert_eq!(a, b) | Built-in macro, no framework needed / 内置宏,无需框架 |
-| ASSERT_NEAR(a, b, eps) | assert!((a - b).abs() < eps) | Or use approx crate |
+| ASSERT_NEAR(a, b, eps) | assert!((a - b).abs() < eps) | Or use approx crate / 或者使用 approx crate |
-| EXPECT_THROW(expr, type) | #[should_panic(expected = "...")] | Or catch_unwind for fine control |
+| EXPECT_THROW(expr, type) | #[should_panic(expected = "...")] | Or catch_unwind / 或使用 catch_unwind 进行精细控制 |
-| EXPECT_DEATH(expr, "msg") | #[should_panic(expected = "msg")] | |
-| EXPECT_DEATH(expr, "msg") | #[should_panic(expected = "msg")] | |
-| class Fixture : public ::testing::Test | Builder functions + Default | No inheritance needed |
+| class Fixture : public ::testing::Test | Builder functions + Default | No inheritance needed / 无需继承 |
-| Google Mock MOCK_METHOD | Trait + test impl | More explicit, no macro magic |
+| Google Mock MOCK_METHOD | Trait + test impl | More explicit, no macro magic / 更显式,无宏魔法 |
-| INSTANTIATE_TEST_SUITE_P (parameterized) | proptest! or macro-generated tests | |
-| INSTANTIATE_TEST_SUITE_P (parameterized) | proptest! or macro-generated tests | |
-| SetUp() / TearDown() | RAII via Drop — cleanup is automatic | Variables dropped at end of test |
+| SetUp() / TearDown() | RAII via Drop — cleanup is automatic | Variables dropped at end of test / 变量在测试结束时被释放 |
-| Separate test binary + CMake | cargo test — zero config | |
-| Separate test binary + CMake | cargo test — zero config / 零配置 | |
-| ctest --output-on-failure | cargo test -- --nocapture | |
-| ctest --output-on-failure | cargo test -- --nocapture | |
- Unit tests live inside
#[cfg(test)]modules alongside your code. Integration tests live in a separatetests/directory at the crate root and test your library’s public API as an external consumer would:
- 单元测试位于代码旁边的
#[cfg(test)]模块中。集成测试位于 crate 根目录下的独立tests/目录中,并像外部消费者一样测试你的库的公有 API:
- my_crate/
+ my_crate/ # 你的 crate
├── src/
- │ └── lib.rs # Your library code
+ │ └── lib.rs # Your library code / 你的库代码
├── tests/
- │ ├── smoke.rs # Each .rs file is a separate test binary
+ │ ├── smoke.rs # Each .rs file is a separate test binary / 每个 .rs 文件都是一个独立的测试二进制文件
- │ ├── regression.rs
- │ └── common/
- │ └── mod.rs # Shared test helpers (NOT a test itself)
+ │ ├── regression.rs # 回归测试
+ │ └── common/ # 测试辅助工具
+ │ └── mod.rs # Shared test helpers (NOT a test itself) / 共享测试辅助工具(其本身不是测试)
└── Cargo.toml
#![allow(unused)]
fn main() {
- // tests/smoke.rs — tests your crate as an external user would
+ // tests/smoke.rs — tests your crate as an external user would / 像外部用户一样测试你的 crate
- use my_crate::DiagEngine; // Only public API is accessible
+ use my_crate::DiagEngine; // Only public API is accessible / 仅可访问公有 API
#[test]
fn engine_starts_successfully() {
let engine = DiagEngine::new("test_config.json");
assert!(engine.is_ok());
}
#[test]
fn engine_rejects_invalid_config() {
let engine = DiagEngine::new("nonexistent.json");
assert!(engine.is_err());
}
}
#![allow(unused)]
fn main() {
- // tests/common/mod.rs — shared helpers, NOT compiled as a test binary
+ // tests/common/mod.rs — shared helpers / 共享辅助工具,不会被编译为测试二进制文件
pub fn setup_test_environment() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("config.json"), r#"{"log_level": "debug"}"#).unwrap();
dir
}
}
#![allow(unused)]
fn main() {
- // tests/regression.rs — can use shared helpers
+ // tests/regression.rs — can use shared helpers / 可以使用共享辅助工具
mod common;
#[test]
fn regression_issue_42() {
let env = common::setup_test_environment();
let engine = my_crate::DiagEngine::new(
env.path().join("config.json").to_str().unwrap()
);
assert!(engine.is_ok());
}
}
- Running integration tests:
- Running integration tests / 运行集成测试:
- cargo test # Runs unit AND integration tests
+ cargo test # Runs unit AND integration tests / 运行单元测试和集成测试
- cargo test --test smoke # Run only tests/smoke.rs
+ cargo test --test smoke # Run only tests/smoke.rs / 仅运行 tests/smoke.rs
- cargo test --test regression # Run only tests/regression.rs
+ cargo test --test regression # Run only tests/regression.rs / 仅运行 tests/regression.rs
- cargo test --lib # Run ONLY unit tests (skip integration)
+ cargo test --lib # Run ONLY unit tests (skip integration) / 仅运行单元测试(跳过集成测试)
-
Key difference from unit tests: Integration tests cannot access private functions or
pub(crate)items. This forces you to verify that your public API is sufficient — a valuable design signal. In C++ terms, it’s like testing against only the public header with nofriendaccess.
-
与单元测试的关键区别:集成测试无法访问私有函数或
pub(crate)项。这会强制你验证公有 API 是否足够 —— 这是一个宝贵的辅助设计的信号。用 C++ 的话来说,这就像是在没有任何friend访问权限的情况下,仅针对公有头文件进行测试。