rust-testing by affaan-m/everything-claude-code
npx skills add https://github.com/affaan-m/everything-claude-code --skill rust-testing遵循 TDD 方法论编写可靠、可维护测试的全面 Rust 测试模式。
#[cfg(test)] 模块中使用 #[test],使用 rstest 进行参数化测试,或使用 proptest 进行基于属性的测试RED → 首先编写一个失败的测试
GREEN → 编写最少的代码以通过测试
REFACTOR → 改进代码,同时保持测试通过
REPEAT → 继续下一个需求
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// RED: 先写测试,使用 todo!() 作为占位符
pub fn add(a: i32, b: i32) -> i32 { todo!() }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() { assert_eq!(add(2, 3), 5); }
}
// cargo test → 在 'not yet implemented' 处 panic
// GREEN: 用最少的实现替换 todo!()
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test → 通过,然后在保持测试通过的情况下进行 REFACTOR
// src/user.rs
pub struct User {
pub name: String,
pub email: String,
}
impl User {
pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, String> {
let email = email.into();
if !email.contains('@') {
return Err(format!("invalid email: {email}"));
}
Ok(Self { name: name.into(), email })
}
pub fn display_name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_user_with_valid_email() {
let user = User::new("Alice", "alice@example.com").unwrap();
assert_eq!(user.display_name(), "Alice");
assert_eq!(user.email, "alice@example.com");
}
#[test]
fn rejects_invalid_email() {
let result = User::new("Bob", "not-an-email");
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid email"));
}
}
assert_eq!(2 + 2, 4); // 相等性
assert_ne!(2 + 2, 5); // 不等性
assert!(vec![1, 2, 3].contains(&2)); // 布尔值
assert_eq!(value, 42, "expected 42 but got {value}"); // 自定义消息
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // 浮点数比较
Result 返回值#[test]
fn parse_returns_error_for_invalid_input() {
let result = parse_config("}{invalid");
assert!(result.is_err());
// 断言特定的错误变体
let err = result.unwrap_err();
assert!(matches!(err, ConfigError::ParseError(_)));
}
#[test]
fn parse_succeeds_for_valid_input() -> Result<(), Box<dyn std::error::Error>> {
let config = parse_config(r#"{"port": 8080}"#)?;
assert_eq!(config.port, 8080);
Ok(()) // 如果任何 ? 返回 Err,则测试失败
}
#[test]
#[should_panic]
fn panics_on_empty_input() {
process(&[]);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn panics_with_specific_message() {
let v: Vec<i32> = vec![];
let _ = v[0];
}
my_crate/
├── src/
│ └── lib.rs
├── tests/ # 集成测试
│ ├── api_test.rs # 每个文件都是一个独立的测试二进制文件
│ ├── db_test.rs
│ └── common/ # 共享的测试工具
│ └── mod.rs
// tests/api_test.rs
use my_crate::{App, Config};
#[test]
fn full_request_lifecycle() {
let config = Config::test_default();
let app = App::new(config);
let response = app.handle_request("/health");
assert_eq!(response.status, 200);
assert_eq!(response.body, "OK");
}
#[tokio::test]
async fn fetches_data_successfully() {
let client = TestClient::new().await;
let result = client.get("/data").await;
assert!(result.is_ok());
assert_eq!(result.unwrap().items.len(), 3);
}
#[tokio::test]
async fn handles_timeout() {
use std::time::Duration;
let result = tokio::time::timeout(
Duration::from_millis(100),
slow_operation(),
).await;
assert!(result.is_err(), "should have timed out");
}
rstest 进行参数化测试use rstest::{rstest, fixture};
#[rstest]
#[case("hello", 5)]
#[case("", 0)]
#[case("rust", 4)]
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
assert_eq!(input.len(), expected);
}
// 夹具
#[fixture]
fn test_db() -> TestDb {
TestDb::new_in_memory()
}
#[rstest]
fn test_insert(test_db: TestDb) {
test_db.insert("key", "value");
assert_eq!(test_db.get("key"), Some("value".into()));
}
#[cfg(test)]
mod tests {
use super::*;
/// 使用合理的默认值创建测试用户。
fn make_user(name: &str) -> User {
User::new(name, &format!("{name}@test.com")).unwrap()
}
#[test]
fn user_display() {
let user = make_user("alice");
assert_eq!(user.display_name(), "alice");
}
}
proptest 进行基于属性的测试use proptest::prelude::*;
proptest! {
#[test]
fn encode_decode_roundtrip(input in ".*") {
let encoded = encode(&input);
let decoded = decode(&encoded).unwrap();
assert_eq!(input, decoded);
}
#[test]
fn sort_preserves_length(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
let original_len = vec.len();
vec.sort();
assert_eq!(vec.len(), original_len);
}
#[test]
fn sort_produces_ordered_output(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
vec.sort();
for window in vec.windows(2) {
assert!(window[0] <= window[1]);
}
}
}
use proptest::prelude::*;
fn valid_email() -> impl Strategy<Value = String> {
("[a-z]{1,10}", "[a-z]{1,5}")
.prop_map(|(user, domain)| format!("{user}@{domain}.com"))
}
proptest! {
#[test]
fn accepts_valid_emails(email in valid_email()) {
assert!(User::new("Test", &email).is_ok());
}
}
mockall 进行模拟use mockall::{automock, predicate::eq};
#[automock]
trait UserRepository {
fn find_by_id(&self, id: u64) -> Option<User>;
fn save(&self, user: &User) -> Result<(), StorageError>;
}
#[test]
fn service_returns_user_when_found() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.with(eq(42))
.times(1)
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
let service = UserService::new(Box::new(mock));
let user = service.get_user(42).unwrap();
assert_eq!(user.name, "Alice");
}
#[test]
fn service_returns_none_when_not_found() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.returning(|_| None);
let service = UserService::new(Box::new(mock));
assert!(service.get_user(99).is_none());
}
/// 将两个数字相加。
///
/// # 示例
///
/// ```
/// use my_crate::add;
///
/// assert_eq!(add(2, 3), 5);
/// assert_eq!(add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// 解析配置字符串。
///
/// # 错误
///
/// 如果输入不是有效的 TOML,则返回 `Err`。
///
/// ```no_run
/// use my_crate::parse_config;
///
/// let config = parse_config(r#"port = 8080"#).unwrap();
/// assert_eq!(config.port, 8080);
/// ```
///
/// ```no_run
/// use my_crate::parse_config;
///
/// assert!(parse_config("}{invalid").is_err());
/// ```
pub fn parse_config(input: &str) -> Result<Config, ParseError> {
todo!()
}
# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "benchmark"
harness = false
// benches/benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
# 安装: cargo install cargo-llvm-cov (或在 CI 中使用 taiki-e/install-action)
cargo llvm-cov # 摘要
cargo llvm-cov --html # HTML 报告
cargo llvm-cov --lcov > lcov.info # 用于 CI 的 LCOV 格式
cargo llvm-cov --fail-under-lines 80 # 如果低于阈值则失败
| 代码类型 | 目标 |
|---|---|
| 关键业务逻辑 | 100% |
| 公共 API | 90%+ |
| 通用代码 | 80%+ |
| 生成的 / FFI 绑定 | 排除 |
cargo test # 运行所有测试
cargo test -- --nocapture # 显示 println 输出
cargo test test_name # 运行匹配模式的测试
cargo test --lib # 仅单元测试
cargo test --test api_test # 仅集成测试
cargo test --doc # 仅文档测试
cargo test --no-fail-fast # 不在第一次失败时停止
cargo test -- --ignored # 运行被忽略的测试
应该:
#[cfg(test)] 模块进行单元测试assert_eq! 而不是 assert!Result 的测试中使用 ? 以获得更清晰的错误输出不应该:
Result::is_err() 时使用 #[should_panic]sleep() — 使用通道、屏障或 tokio::time::pause()# GitHub Actions
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Check formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy -- -D warnings
- name: Run tests
run: cargo test
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Coverage
run: cargo llvm-cov --fail-under-lines 80
记住:测试就是文档。它们展示了你的代码应该如何被使用。清晰地编写它们并保持更新。
每周安装次数
359
仓库
GitHub 星标数
105.0K
首次出现
9 天前
安全审计
安装于
codex343
cursor307
opencode305
gemini-cli304
github-copilot304
amp304
Comprehensive Rust testing patterns for writing reliable, maintainable tests following TDD methodology.
#[test] in a #[cfg(test)] module, rstest for parameterized tests, or proptest for property-based testsRED → Write a failing test first
GREEN → Write minimal code to pass the test
REFACTOR → Improve code while keeping tests green
REPEAT → Continue with next requirement
// RED: Write test first, use todo!() as placeholder
pub fn add(a: i32, b: i32) -> i32 { todo!() }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() { assert_eq!(add(2, 3), 5); }
}
// cargo test → panics at 'not yet implemented'
// GREEN: Replace todo!() with minimal implementation
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test → PASS, then REFACTOR while keeping tests green
// src/user.rs
pub struct User {
pub name: String,
pub email: String,
}
impl User {
pub fn new(name: impl Into<String>, email: impl Into<String>) -> Result<Self, String> {
let email = email.into();
if !email.contains('@') {
return Err(format!("invalid email: {email}"));
}
Ok(Self { name: name.into(), email })
}
pub fn display_name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_user_with_valid_email() {
let user = User::new("Alice", "alice@example.com").unwrap();
assert_eq!(user.display_name(), "Alice");
assert_eq!(user.email, "alice@example.com");
}
#[test]
fn rejects_invalid_email() {
let result = User::new("Bob", "not-an-email");
assert!(result.is_err());
assert!(result.unwrap_err().contains("invalid email"));
}
}
assert_eq!(2 + 2, 4); // Equality
assert_ne!(2 + 2, 5); // Inequality
assert!(vec![1, 2, 3].contains(&2)); // Boolean
assert_eq!(value, 42, "expected 42 but got {value}"); // Custom message
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // Float comparison
Result Returns#[test]
fn parse_returns_error_for_invalid_input() {
let result = parse_config("}{invalid");
assert!(result.is_err());
// Assert specific error variant
let err = result.unwrap_err();
assert!(matches!(err, ConfigError::ParseError(_)));
}
#[test]
fn parse_succeeds_for_valid_input() -> Result<(), Box<dyn std::error::Error>> {
let config = parse_config(r#"{"port": 8080}"#)?;
assert_eq!(config.port, 8080);
Ok(()) // Test fails if any ? returns Err
}
#[test]
#[should_panic]
fn panics_on_empty_input() {
process(&[]);
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn panics_with_specific_message() {
let v: Vec<i32> = vec![];
let _ = v[0];
}
my_crate/
├── src/
│ └── lib.rs
├── tests/ # Integration tests
│ ├── api_test.rs # Each file is a separate test binary
│ ├── db_test.rs
│ └── common/ # Shared test utilities
│ └── mod.rs
// tests/api_test.rs
use my_crate::{App, Config};
#[test]
fn full_request_lifecycle() {
let config = Config::test_default();
let app = App::new(config);
let response = app.handle_request("/health");
assert_eq!(response.status, 200);
assert_eq!(response.body, "OK");
}
#[tokio::test]
async fn fetches_data_successfully() {
let client = TestClient::new().await;
let result = client.get("/data").await;
assert!(result.is_ok());
assert_eq!(result.unwrap().items.len(), 3);
}
#[tokio::test]
async fn handles_timeout() {
use std::time::Duration;
let result = tokio::time::timeout(
Duration::from_millis(100),
slow_operation(),
).await;
assert!(result.is_err(), "should have timed out");
}
rstestuse rstest::{rstest, fixture};
#[rstest]
#[case("hello", 5)]
#[case("", 0)]
#[case("rust", 4)]
fn test_string_length(#[case] input: &str, #[case] expected: usize) {
assert_eq!(input.len(), expected);
}
// Fixtures
#[fixture]
fn test_db() -> TestDb {
TestDb::new_in_memory()
}
#[rstest]
fn test_insert(test_db: TestDb) {
test_db.insert("key", "value");
assert_eq!(test_db.get("key"), Some("value".into()));
}
#[cfg(test)]
mod tests {
use super::*;
/// Creates a test user with sensible defaults.
fn make_user(name: &str) -> User {
User::new(name, &format!("{name}@test.com")).unwrap()
}
#[test]
fn user_display() {
let user = make_user("alice");
assert_eq!(user.display_name(), "alice");
}
}
proptestuse proptest::prelude::*;
proptest! {
#[test]
fn encode_decode_roundtrip(input in ".*") {
let encoded = encode(&input);
let decoded = decode(&encoded).unwrap();
assert_eq!(input, decoded);
}
#[test]
fn sort_preserves_length(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
let original_len = vec.len();
vec.sort();
assert_eq!(vec.len(), original_len);
}
#[test]
fn sort_produces_ordered_output(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
vec.sort();
for window in vec.windows(2) {
assert!(window[0] <= window[1]);
}
}
}
use proptest::prelude::*;
fn valid_email() -> impl Strategy<Value = String> {
("[a-z]{1,10}", "[a-z]{1,5}")
.prop_map(|(user, domain)| format!("{user}@{domain}.com"))
}
proptest! {
#[test]
fn accepts_valid_emails(email in valid_email()) {
assert!(User::new("Test", &email).is_ok());
}
}
mockalluse mockall::{automock, predicate::eq};
#[automock]
trait UserRepository {
fn find_by_id(&self, id: u64) -> Option<User>;
fn save(&self, user: &User) -> Result<(), StorageError>;
}
#[test]
fn service_returns_user_when_found() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.with(eq(42))
.times(1)
.returning(|_| Some(User { id: 42, name: "Alice".into() }));
let service = UserService::new(Box::new(mock));
let user = service.get_user(42).unwrap();
assert_eq!(user.name, "Alice");
}
#[test]
fn service_returns_none_when_not_found() {
let mut mock = MockUserRepository::new();
mock.expect_find_by_id()
.returning(|_| None);
let service = UserService::new(Box::new(mock));
assert!(service.get_user(99).is_none());
}
/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// use my_crate::add;
///
/// assert_eq!(add(2, 3), 5);
/// assert_eq!(add(-1, 1), 0);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Parses a config string.
///
/// # Errors
///
/// Returns `Err` if the input is not valid TOML.
///
/// ```no_run
/// use my_crate::parse_config;
///
/// let config = parse_config(r#"port = 8080"#).unwrap();
/// assert_eq!(config.port, 8080);
/// ```
///
/// ```no_run
/// use my_crate::parse_config;
///
/// assert!(parse_config("}{invalid").is_err());
/// ```
pub fn parse_config(input: &str) -> Result<Config, ParseError> {
todo!()
}
# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "benchmark"
harness = false
// benches/benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}
criterion_group!(benches, bench_fibonacci);
criterion_main!(benches);
# Install: cargo install cargo-llvm-cov (or use taiki-e/install-action in CI)
cargo llvm-cov # Summary
cargo llvm-cov --html # HTML report
cargo llvm-cov --lcov > lcov.info # LCOV format for CI
cargo llvm-cov --fail-under-lines 80 # Fail if below threshold
| Code Type | Target |
|---|---|
| Critical business logic | 100% |
| Public API | 90%+ |
| General code | 80%+ |
| Generated / FFI bindings | Exclude |
cargo test # Run all tests
cargo test -- --nocapture # Show println output
cargo test test_name # Run tests matching pattern
cargo test --lib # Unit tests only
cargo test --test api_test # Integration tests only
cargo test --doc # Doc tests only
cargo test --no-fail-fast # Don't stop on first failure
cargo test -- --ignored # Run ignored tests
DO:
#[cfg(test)] modules for unit testsassert_eq! over assert! for better error messages? in tests that return Result for cleaner error outputDON'T:
#[should_panic] when you can test Result::is_err() insteadsleep() in tests — use channels, barriers, or tokio::time::pause()# GitHub Actions
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- name: Check formatting
run: cargo fmt --check
- name: Clippy
run: cargo clippy -- -D warnings
- name: Run tests
run: cargo test
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Coverage
run: cargo llvm-cov --fail-under-lines 80
Remember : Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date.
Weekly Installs
359
Repository
GitHub Stars
105.0K
First Seen
9 days ago
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex343
cursor307
opencode305
gemini-cli304
github-copilot304
amp304
Spring Boot工程师技能指南:微服务架构、安全加固与云原生开发实战
2,800 周安装
Rust调用关系图生成器 - 可视化函数调用层次结构,提升代码分析效率
539 周安装
parallel-web-extract:并行网页内容提取工具,高效抓取网页数据
595 周安装
腾讯云CloudBase AI模型Web技能:前端调用混元/DeepSeek模型,实现流式文本生成
560 周安装
Apollo Connectors 模式助手:GraphQL API 连接器开发与集成指南
565 周安装
GitHub Trending 趋势分析工具:实时发现热门项目、技术洞察与开源机会
556 周安装
GSAP React 集成教程:useGSAP Hook 动画库与 React 组件开发指南
546 周安装