引言
cargo run 和 cargo test 是 Rust 开发工作流中最常用的两个命令,它们分别代表了执行和验证两个核心环节。cargo run 不仅编译项目,还立即运行生成的二进制文件,是快速迭代开发的利器。cargo test 则运行测试套件,确保代码质量和正确性,是 Rust 强调可靠性的体现。这两个命令背后蕴含着复杂的机制------从编译优化、参数传递到测试发现、并行执行,理解它们的工作原理对于构建高质量 Rust 项目至关重要。更深层次地,它们体现了 Rust 社区对开发体验和代码质量的双重追求。
Cargo Run:快速执行的艺术
cargo run 本质上是 cargo build 加上执行步骤的组合。它首先编译项目(如果需要),然后运行生成的可执行文件。默认情况下,它使用 debug 配置构建,优先编译速度而非运行性能。这种设计哲学体现了开发阶段快速反馈的重要性。
命令的参数传递机制很灵活。cargo run 后的所有参数都会传递给可执行文件,而非 cargo 本身。如果需要传递参数给 cargo,需要在 -- 分隔符之前指定。例如 cargo run --release -- arg1 arg2 中,--release 是 cargo 参数,arg1 arg2 是程序参数。
多二进制项目中,cargo run 默认运行 src/main.rs 对应的二进制。如果项目有多个二进制目标(在 src/bin/ 目录或通过 [[bin]] 配置),需要使用 --bin 参数指定:cargo run --bin cli-tool。这种组织方式允许单个项目包含多个相关工具。
cargo run 的一个强大特性是它会自动处理增量编译。如果代码未修改,会跳过编译直接运行。如果只有部分文件修改,只重新编译受影响的部分。这使得迭代开发非常快速,尤其是在大型项目中。
环境变量可以通过 shell 传递,但 cargo 也支持 .cargo/config.toml 中配置环境变量。这对于需要特定配置的项目很有用,如数据库连接字符串或 API 密钥(虽然敏感信息应该用其他方式管理)。
Cargo Test:质量保证的基石
cargo test 是 Rust 测试基础设施的入口,它会发现并运行项目中的所有测试。Rust 的测试体系包括三种类型:单元测试(与代码在同一文件)、集成测试(在 tests/ 目录)和文档测试(在文档注释中)。cargo test 会运行所有这些测试,确保全面覆盖。
测试发现是自动的。任何标记 #[test] 的函数都会被识别为测试。#[cfg(test)] 模块包含测试代码,这些代码只在测试时编译,不会进入最终二进制。这种设计既方便又高效------测试代码与被测代码紧密相邻,但不会增加生产环境的二进制大小。
并行执行是 cargo test 的默认行为。多个测试会在不同线程中并发运行,充分利用多核 CPU 加速测试套件。但这要求测试之间相互独立,不能共享可变状态。如果测试有顺序依赖或需要独占资源,可以使用 cargo test -- --test-threads=1 串行执行。
测试过滤允许只运行特定测试。cargo test test_name 运行名称包含 test_name 的测试。cargo test mod_name:: 运行特定模块的测试。这在调试特定功能时非常有用,避免运行整个测试套件。
--ignored 参数运行被 #[ignore] 标记的测试。这些通常是慢速测试或需要特殊环境的测试。--include-ignored 运行所有测试包括被忽略的。--lib 只运行库测试,--bins 只运行二进制测试,--doc 只运行文档测试。
测试输出默认被捕获,只在测试失败时显示。-- --nocapture 参数显示所有输出,-- --show-output 显示成功测试的输出。这对于调试测试本身很有用。
深度实践:构建完整的测试和运行工作流
下面通过实际项目展示两个命令的高级用法:
toml
# Cargo.toml
[package]
name = "calculator"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
[dev-dependencies]
criterion = "0.5"
proptest = "1.4"
[[bin]]
name = "calc"
path = "src/bin/calc.rs"
[[bin]]
name = "calc-server"
path = "src/bin/server.rs"
[[bench]]
name = "operations"
harness = false
rust
// src/lib.rs
//! 计算器库
//!
//! 提供基本的数学运算功能。
//!
//! # Examples
//!
//! ```
//! use calculator::Calculator;
//!
//! let calc = Calculator::new();
//! assert_eq!(calc.add(2, 3), 5);
//! ```
/// 计算器结构体
#[derive(Debug, Clone)]
pub struct Calculator {
precision: u32,
}
impl Calculator {
/// 创建新的计算器
///
/// # Examples
///
/// ```
/// use calculator::Calculator;
/// let calc = Calculator::new();
/// ```
pub fn new() -> Self {
Self { precision: 2 }
}
/// 设置精度
pub fn with_precision(precision: u32) -> Self {
Self { precision }
}
/// 加法运算
///
/// # Examples
///
/// ```
/// use calculator::Calculator;
/// let calc = Calculator::new();
/// assert_eq!(calc.add(10, 20), 30);
/// ```
pub fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
/// 减法运算
pub fn subtract(&self, a: i32, b: i32) -> i32 {
a - b
}
/// 乘法运算
pub fn multiply(&self, a: i32, b: i32) -> i32 {
a * b
}
/// 除法运算
///
/// # Panics
///
/// 当除数为零时 panic
///
/// # Examples
///
/// ```
/// use calculator::Calculator;
/// let calc = Calculator::new();
/// assert_eq!(calc.divide(10, 2), 5);
/// ```
///
/// ```should_panic
/// use calculator::Calculator;
/// let calc = Calculator::new();
/// calc.divide(10, 0); // panics
/// ```
pub fn divide(&self, a: i32, b: i32) -> i32 {
if b == 0 {
panic!("除数不能为零");
}
a / b
}
/// 计算表达式
pub fn evaluate(&self, expression: &str) -> Result<i32, String> {
// 简化的表达式解析
let parts: Vec<&str> = expression.split_whitespace().collect();
if parts.len() != 3 {
return Err("无效表达式".to_string());
}
let a: i32 = parts[0].parse().map_err(|_| "无效数字")?;
let b: i32 = parts[2].parse().map_err(|_| "无效数字")?;
match parts[1] {
"+" => Ok(self.add(a, b)),
"-" => Ok(self.subtract(a, b)),
"*" => Ok(self.multiply(a, b)),
"/" => Ok(self.divide(a, b)),
_ => Err("未知运算符".to_string()),
}
}
}
impl Default for Calculator {
fn default() -> Self {
Self::new()
}
}
// === 单元测试 ===
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let calc = Calculator::new();
assert_eq!(calc.add(2, 3), 5);
assert_eq!(calc.add(-1, 1), 0);
assert_eq!(calc.add(0, 0), 0);
}
#[test]
fn test_subtract() {
let calc = Calculator::new();
assert_eq!(calc.subtract(5, 3), 2);
assert_eq!(calc.subtract(3, 5), -2);
}
#[test]
fn test_multiply() {
let calc = Calculator::new();
assert_eq!(calc.multiply(3, 4), 12);
assert_eq!(calc.multiply(-2, 3), -6);
}
#[test]
fn test_divide() {
let calc = Calculator::new();
assert_eq!(calc.divide(10, 2), 5);
assert_eq!(calc.divide(7, 2), 3); // 整数除法
}
#[test]
#[should_panic(expected = "除数不能为零")]
fn test_divide_by_zero() {
let calc = Calculator::new();
calc.divide(10, 0);
}
#[test]
fn test_evaluate() {
let calc = Calculator::new();
assert_eq!(calc.evaluate("10 + 5").unwrap(), 15);
assert_eq!(calc.evaluate("10 - 5").unwrap(), 5);
assert_eq!(calc.evaluate("10 * 5").unwrap(), 50);
assert_eq!(calc.evaluate("10 / 5").unwrap(), 2);
}
#[test]
fn test_evaluate_error() {
let calc = Calculator::new();
assert!(calc.evaluate("invalid").is_err());
assert!(calc.evaluate("10 +").is_err());
}
#[test]
#[ignore = "慢速测试"]
fn test_large_computation() {
let calc = Calculator::new();
let mut result = 0;
for i in 0..1_000_000 {
result = calc.add(result, i);
}
assert!(result > 0);
}
}
rust
// src/bin/calc.rs
use calculator::Calculator;
use clap::Parser;
/// 命令行计算器
#[derive(Parser, Debug)]
#[command(name = "calc")]
#[command(about = "简单的命令行计算器", long_about = None)]
struct Args {
/// 第一个数字
#[arg(short, long)]
a: i32,
/// 运算符 (+, -, *, /)
#[arg(short, long)]
op: String,
/// 第二个数字
#[arg(short, long)]
b: i32,
/// 显示详细信息
#[arg(short, long)]
verbose: bool,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
if args.verbose {
println!("计算器 v{}", env!("CARGO_PKG_VERSION"));
println!("计算: {} {} {}", args.a, args.op, args.b);
}
let calc = Calculator::new();
let expression = format!("{} {} {}", args.a, args.op, args.b);
match calc.evaluate(&expression) {
Ok(result) => {
println!("{}", result);
Ok(())
}
Err(e) => {
eprintln!("错误: {}", e);
std::process::exit(1);
}
}
}
rust
// src/bin/server.rs
use calculator::Calculator;
fn main() {
println!("计算器服务器启动...");
println!("版本: {}", env!("CARGO_PKG_VERSION"));
let calc = Calculator::new();
// 模拟服务器逻辑
loop {
println!("等待请求...");
std::thread::sleep(std::time::Duration::from_secs(5));
// 示例计算
let result = calc.add(1, 2);
println!("计算结果: {}", result);
break; // 演示用,实际会持续运行
}
}
rust
// tests/integration_test.rs
use calculator::Calculator;
#[test]
fn test_calculator_integration() {
let calc = Calculator::new();
// 测试一系列操作
let result1 = calc.add(10, 20);
let result2 = calc.multiply(result1, 2);
let result3 = calc.divide(result2, 3);
assert_eq!(result3, 20);
}
#[test]
fn test_expression_evaluation() {
let calc = Calculator::new();
let expressions = vec![
("5 + 3", 8),
("10 - 4", 6),
("6 * 7", 42),
("20 / 4", 5),
];
for (expr, expected) in expressions {
assert_eq!(
calc.evaluate(expr).unwrap(),
expected,
"表达式 {} 计算错误",
expr
);
}
}
#[test]
fn test_precision_settings() {
let calc1 = Calculator::new();
let calc2 = Calculator::with_precision(4);
// 两个计算器应该产生相同的整数结果
assert_eq!(calc1.add(1, 2), calc2.add(1, 2));
}
rust
// benches/operations.rs
use calculator::Calculator;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn benchmark_add(c: &mut Criterion) {
let calc = Calculator::new();
c.bench_function("add", |b| {
b.iter(|| calc.add(black_box(100), black_box(200)))
});
}
fn benchmark_evaluate(c: &mut Criterion) {
let calc = Calculator::new();
c.bench_function("evaluate", |b| {
b.iter(|| calc.evaluate(black_box("100 + 200")))
});
}
criterion_group!(benches, benchmark_add, benchmark_evaluate);
criterion_main!(benches);
bash
#!/bin/bash
# test_and_run.sh - 测试和运行脚本
echo "=== Cargo Run 与 Test 综合演示 ==="
# 1. 运行所有测试
echo -e "\n--- 1. 运行所有测试 ---"
cargo test
# 2. 运行特定测试
echo -e "\n--- 2. 运行特定测试 ---"
cargo test test_add
# 3. 显示测试输出
echo -e "\n--- 3. 显示测试输出 ---"
cargo test test_add -- --nocapture
# 4. 运行被忽略的测试
echo -e "\n--- 4. 运行被忽略的测试 ---"
cargo test -- --ignored
# 5. 运行文档测试
echo -e "\n--- 5. 运行文档测试 ---"
cargo test --doc
# 6. 运行集成测试
echo -e "\n--- 6. 运行集成测试 ---"
cargo test --test integration_test
# 7. 串行运行测试
echo -e "\n--- 7. 串行运行测试 ---"
cargo test -- --test-threads=1
# 8. 运行默认二进制
echo -e "\n--- 8. 运行默认二进制 ---"
cargo run -- -a 10 -o + -b 20
# 9. 运行指定二进制
echo -e "\n--- 9. 运行计算器 CLI ---"
cargo run --bin calc -- -a 15 -o "*" -b 3 --verbose
# 10. 运行服务器
echo -e "\n--- 10. 运行服务器 (2秒后终止) ---"
timeout 2 cargo run --bin calc-server || true
# 11. 发布模式运行
echo -e "\n--- 11. 发布模式运行 ---"
cargo run --release --bin calc -- -a 100 -o "/" -b 5
# 12. 运行基准测试
echo -e "\n--- 12. 运行基准测试 ---"
cargo bench
# 13. 查看测试覆盖率(需要 cargo-tarpaulin)
echo -e "\n--- 13. 测试覆盖率 ---"
if command -v cargo-tarpaulin &> /dev/null; then
cargo tarpaulin --out Html
echo "查看 tarpaulin-report.html"
else
echo "未安装 cargo-tarpaulin"
fi
# 14. 运行示例
echo -e "\n--- 14. 运行示例 ---"
cargo run --example basic 2>/dev/null || echo "无示例文件"
rust
// examples/basic.rs
use calculator::Calculator;
fn main() {
println!("=== 计算器基本用法示例 ===\n");
let calc = Calculator::new();
println!("加法: 10 + 5 = {}", calc.add(10, 5));
println!("减法: 10 - 5 = {}", calc.subtract(10, 5));
println!("乘法: 10 * 5 = {}", calc.multiply(10, 5));
println!("除法: 10 / 5 = {}", calc.divide(10, 5));
println!("\n表达式求值:");
match calc.evaluate("42 + 8") {
Ok(result) => println!("42 + 8 = {}", result),
Err(e) => eprintln!("错误: {}", e),
}
}
实践中的专业思考
测试组织的策略 :单元测试与代码同文件便于维护,集成测试在 tests/ 目录测试公共 API,文档测试既是文档又是测试。三者结合提供全面覆盖。
测试并行化的权衡 :并行执行加速测试但要求测试独立。共享资源(如文件、数据库)的测试应该串行或使用隔离机制。--test-threads 控制并发度。
参数传递的清晰性 :cargo run -- args 的 -- 分隔符明确区分了 cargo 参数和程序参数。这种设计避免了歧义,是良好的命令行界面设计。
多二进制的应用:单个项目可以包含多个相关工具,共享库代码。这种组织方式既保持了代码复用,又提供了灵活性。
测试过滤的实用性 :开发特定功能时只运行相关测试,避免等待整个测试套件。cargo test module_name::test_name 提供精确控制。
基准测试的集成 :Criterion 集成到 cargo bench 工作流,提供可靠的性能测试。基准测试应该与功能测试分离,因为它们有不同的目的。
文档测试的双重价值:文档测试确保文档中的示例代码可运行,同时也测试了 API 的可用性。这是"代码即文档"理念的完美体现。
高级技巧与最佳实践
条件测试 :使用 #[cfg(test)] 和 #[cfg(not(test))] 实现仅测试时可见的辅助函数,避免污染生产代码。
测试夹具 :通过 setup 和 teardown 函数(或使用 Drop trait)管理测试资源,确保测试后清理。
属性测试 :使用 proptest 进行基于属性的测试,生成大量随机输入验证不变量。这能发现边界情况的 bug。
集成测试的隔离:每个集成测试文件独立编译为二进制,避免测试间的耦合。但这也意味着编译时间更长。
运行时环境配置:通过环境变量或配置文件控制程序行为,使得同一二进制在不同环境中表现不同。
常见问题与解决方案
测试超时 :长时间运行的测试可能被杀死。可以通过 --timeout 参数或在测试中使用 tokio::time::timeout 控制。
测试输出混乱 :并行测试的输出可能交错。使用 --test-threads=1 串行运行或使用结构化日志(如 tracing)。
二进制选择错误 :多二进制项目中需要明确指定 --bin 参数,否则 cargo 可能运行错误的二进制。
测试依赖的版本冲突 :dev-dependencies 可能与主依赖冲突。使用工作空间统一管理依赖版本。
结语
cargo run 和 cargo test 是 Rust 开发工作流的双引擎,分别驱动着快速迭代和质量保证。cargo run 的增量编译和灵活的参数传递使得开发体验流畅,cargo test 的全面测试发现和并行执行确保代码质量。理解这两个命令的工作机制------从测试组织、过滤、并行化到程序执行、参数传递和多二进制管理------是掌握 Rust 开发工作流的关键。结合文档测试、集成测试和基准测试,我们可以构建既高质量又高性能的 Rust 项目。这正是 Rust 语言"无畏并发、内存安全"承诺在工程实践中的体现。