C 项目里测试常从一个额外的 main 开始:
#include <assert.h>
int main(void)
{
assert(checksum((uint8_t[]){1, 2, 3}, 3) == 6);
return 0;
}
或者写几个小程序、shell 脚本、Makefile 目标:
test:
./test_checksum
./test_parser
Rust 把这个入口直接放进 Cargo:
cargo test
第一层可以先记住:Rust 的测试不是另外一套工程系统,而是 crate 的一部分。
最小单元测试
一个普通函数:
fn checksum(data: &[u8]) -> u32 {
data.iter().map(|b| *b as u32).sum()
}
旁边可以写测试:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checksum_sums_bytes() {
assert_eq!(checksum(&[1, 2, 3]), 6);
}
}
运行:
cargo test
这里有几个点:
#[test]标记测试函数assert_eq!比较左右值#[cfg(test)]表示这段模块只在测试构建里编译use super::*让测试能访问当前模块里的函数
这相当于把 C 里的测试 main 收进源文件附近。
测试可以访问私有函数
C 里测试静态函数常有点麻烦:要么暴露到头文件,要么把 .c 文件包含进测试程序。
Rust 模块内测试可以访问同一模块的私有项:
fn parse_len(data: &[u8]) -> Option<u16> {
if data.len() < 2 {
return None;
}
Some(u16::from_be_bytes([data[0], data[1]]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_len_reads_big_endian() {
assert_eq!(parse_len(&[0x01, 0x02]), Some(258));
}
}
这让你不用为了测试而随便把内部函数改成公开 API。
断言和失败信息
Rust 常用断言:
assert!(value > 0);
assert_eq!(actual, expected);
assert_ne!(left, right);
可以加失败信息:
assert_eq!(actual, expected, "bad packet length for {case_name}");
测试返回 Result 也可以:
#[test]
fn load_config() -> Result<(), Box<dyn std::error::Error>> {
let text = std::fs::read_to_string("testdata/config.toml")?;
assert!(text.contains("port"));
Ok(())
}
这样测试里可以用 ?。失败会被测试框架报告出来。
测试错误路径
C 测试容易只覆盖成功路径。Rust 的 Result 和 Option 很适合直接测错误分支:
#[derive(Debug, PartialEq, Eq)]
enum ParseError {
TooShort,
BadVersion(u8),
}
fn parse_packet(data: &[u8]) -> Result<(), ParseError> {
if data.len() < 2 {
return Err(ParseError::TooShort);
}
if data[0] != 1 {
return Err(ParseError::BadVersion(data[0]));
}
Ok(())
}
#[test]
fn parse_rejects_short_packet() {
assert_eq!(parse_packet(&[1]), Err(ParseError::TooShort));
}
为了比较错误值,给错误类型派生 PartialEq / Eq 很常见。错误类型如果包含 io::Error 这类不方便直接比较的字段,可以只检查分类或消息中稳定的部分。
集成测试放 tests/
模块内测试适合测内部逻辑。对外 API 可以放到 tests/:
mycrate/
├── src/
│ └── lib.rs
└── tests/
└── parser_test.rs
tests/parser_test.rs 像外部用户一样使用 crate:
use mycrate::parse_packet;
#[test]
fn parses_valid_packet() {
let packet = [1_u8, 0];
assert!(parse_packet(&packet).is_ok());
}
集成测试只能访问公开 API。这和 C 里用头文件暴露出来的接口做黑盒测试很像。
如果你的项目最初只有 src/main.rs,想写更容易测试的代码,可以把核心逻辑挪到 src/lib.rs,让 main.rs 只处理 CLI 边界。
测试 CLI 工具
最小方式是把业务逻辑放到 run 或库函数里测试,不直接测试进程:
pub fn line_count(text: &str) -> usize {
text.lines().count()
}
#[test]
fn counts_lines() {
assert_eq!(line_count("a\nb\n"), 2);
}
CLI 的 stdout、stderr、退出码可以后面用集成测试或专门工具测。第一层先避免把所有逻辑都塞进 main,否则测试会变难。
这个结构和 C 项目里“核心逻辑做成库,main 只负责参数和输出”是同一个经验。
testdata 放固定输入
解析器、协议、文件格式通常需要固定样本:
tests/
├── parser_test.rs
└── testdata/
├── valid.bin
└── bad-version.bin
测试里读取:
#[test]
fn parses_sample() -> Result<(), Box<dyn std::error::Error>> {
let data = std::fs::read("tests/testdata/valid.bin")?;
assert!(parse_packet(&data).is_ok());
Ok(())
}
测试数据应该稳定、可版本管理。不要让测试依赖开发者机器上的临时文件、当前目录里的偶然状态或网络服务。
运行部分测试
常用命令:
cargo test
cargo test checksum
cargo test -- --nocapture
含义:
cargo test -> 运行所有测试
cargo test checksum -> 只运行名字包含 checksum 的测试
-- --nocapture -> 显示测试里的 stdout/stderr
如果测试较多,给测试函数起清楚的名字很重要:
#[test]
fn parse_rejects_bad_version() {}
#[test]
fn parse_rejects_short_packet() {}
失败时,测试名就是第一条定位线索。
这一层先记住什么
C 到 Rust 的测试迁移第一层:
手写 test_main.c -> #[test] + cargo test
assert / assert.h -> assert! / assert_eq! / assert_ne!
测试静态函数 -> 模块内 #[cfg(test)] tests
黑盒测试公开 API -> tests/ 集成测试
测试文件样本 -> tests/testdata/
测试可失败操作 -> 测试函数返回 Result
只测 main 很困难 -> 把核心逻辑放进 lib.rs / 普通函数
Rust 的测试入口从项目一开始就存在。对 C 开发者来说,最重要的迁移不是学会 #[test] 这个属性,而是把“可测试的核心逻辑”和“进程边界 main”分开。