
Rust Testing Patterns
FreeReliable testing patterns for Rust development.
Free · Opens the source repo
What Rust Testing Patterns does
Rust Testing Patterns provides a comprehensive framework for writing reliable and maintainable tests in Rust, adhering to Test-Driven Development (TDD) methodology. This skill is designed for developers looking to implement various testing strategies, including unit tests, integration tests, asynchronous tests, property-based tests, and mocking. By following the structured approach outlined in this skill, you can ensure that your Rust code is thoroughly tested and meets performance requirements.
The skill guides users through the TDD cycle, emphasizing the importance of writing tests before implementation. It covers essential steps, such as identifying the target code, writing tests using Rust's built-in testing framework, and utilizing libraries like mockall for dependency mocking. This structured workflow not only helps in catching errors early but also promotes cleaner and more maintainable code.
Additionally, the skill includes examples of various testing scenarios, such as testing for valid and invalid inputs, handling asynchronous operations, and organizing tests effectively. It also emphasizes the significance of code coverage, encouraging developers to aim for at least 80% coverage to ensure robustness. Overall, Rust Testing Patterns is an invaluable resource for any Rust developer aiming to improve their testing practices and code quality.
When to use it
Use this skill when developing new features or adding test coverage to existing Rust code, especially when following TDD practices.
When not to use it
This skill may not be suitable for projects that do not require rigorous testing or for developers unfamiliar with Rust's testing framework.
What you can build with it
Writing New Functions in Rust
Utilize this skill when creating new functions or methods in Rust to ensure they are properly tested from the start.
Adding Test Coverage
Incorporate this skill to enhance existing Rust code with necessary test coverage, following TDD practices.
Creating Performance Benchmarks
Use this skill to establish benchmarks for Rust code that requires performance validation.
How to install Rust Testing Patterns
View source1. Install with the skills CLI
npx skills add affaan-m/ecc/rust-testing --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by affaan-mPatrones de Pruebas Rust
Patrones completos de pruebas en Rust para escribir pruebas confiables y mantenibles siguiendo la metodología TDD.
Cuándo Usar
- Escribir nuevas funciones, métodos o traits en Rust
- Agregar cobertura de pruebas a código existente
- Crear benchmarks para código con requisitos de rendimiento
- Implementar pruebas basadas en propiedades para validación de entrada
- Seguir el flujo de trabajo TDD en proyectos Rust
Cómo Funciona
- Identificar el código objetivo — Encontrar la función, trait o módulo a probar
- Escribir una prueba — Usar
#[test]en un módulo#[cfg(test)], rstest para pruebas parametrizadas, o proptest para pruebas basadas en propiedades - Mockear dependencias — Usar mockall para aislar la unidad bajo prueba
- Ejecutar pruebas (ROJO) — Verificar que la prueba falla con el error esperado
- Implementar (VERDE) — Escribir el código mínimo para que pase
- Refactorizar — Mejorar mientras se mantienen las pruebas en verde
- Verificar cobertura — Usar cargo-llvm-cov, objetivo 80%+
Flujo de Trabajo TDD en Rust
El Ciclo ROJO-VERDE-REFACTORIZAR
ROJO → Escribir primero una prueba que falle
VERDE → Escribir el código mínimo para que pase
REFACTORIZAR → Mejorar el código manteniendo las pruebas en verde
REPETIR → Continuar con el siguiente requisito
TDD Paso a Paso en Rust
// ROJO: Escribir la prueba primero, usar todo!() como 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 → panic en 'not yet implemented'
// VERDE: Reemplazar todo!() con implementación mínima
pub fn add(a: i32, b: i32) -> i32 { a + b }
// cargo test → PASS, luego REFACTORIZAR manteniendo pruebas en verde
Pruebas Unitarias
Organización de Pruebas a Nivel de Módulo
// 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"));
}
}
Macros de Aserción
assert_eq!(2 + 2, 4); // Igualdad
assert_ne!(2 + 2, 5); // Desigualdad
assert!(vec![1, 2, 3].contains(&2)); // Booleano
assert_eq!(value, 42, "expected 42 but got {value}"); // Mensaje personalizado
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // Comparación de flotantes
Pruebas de Errores y Panics
Probar Retornos de Result
#[test]
fn parse_returns_error_for_invalid_input() {
let result = parse_config("}{invalid");
assert!(result.is_err());
// Verificar variante de error específica
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(()) // La prueba falla si algún ? retorna Err
}
Probar Panics
#[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];
}
Pruebas de Integración
Estructura de Archivos
my_crate/
├── src/
│ └── lib.rs
├── tests/ # Pruebas de integración
│ ├── api_test.rs # Cada archivo es un binario de prueba separado
│ ├── db_test.rs
│ └── common/ # Utilidades de prueba compartidas
│ └── mod.rs
Escribir Pruebas de Integración
// 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");
}
Pruebas Async
Con Tokio
#[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");
}
Patrones de Organización de Pruebas
Pruebas Parametrizadas con 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);
}
// 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()));
}
Helpers de Prueba
#[cfg(test)]
mod tests {
use super::*;
/// Crea un usuario de prueba con valores predeterminados sensatos.
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");
}
}
Pruebas Basadas en Propiedades con proptest
Pruebas de Propiedades Básicas
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]);
}
}
}
Estrategias Personalizadas
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());
}
}
Mocking con mockall
Mocking Basado en Traits
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());
}
Pruebas de Documentación
Documentación Ejecutable
/// Suma dos números.
///
/// # 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
}
/// Parsea una cadena de configuración.
///
/// # Errors
///
/// Retorna `Err` si la entrada no es TOML válido.
///
/// ```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!()
}
Benchmarks con Criterion
# 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);
Cobertura de Pruebas
Ejecutar Cobertura
# Instalar: cargo install cargo-llvm-cov (o usar taiki-e/install-action en CI)
cargo llvm-cov # Resumen
cargo llvm-cov --html # Reporte HTML
cargo llvm-cov --lcov > lcov.info # Formato LCOV para CI
cargo llvm-cov --fail-under-lines 80 # Fallar si está por debajo del umbral
Objetivos de Cobertura
| Tipo de Código | Objetivo |
|---|---|
| Lógica de negocio crítica | 100% |
| API pública | 90%+ |
| Código general | 80%+ |
| Bindings generados / FFI | Excluir |
Comandos de Prueba
cargo test # Ejecutar todas las pruebas
cargo test -- --nocapture # Mostrar salida de println
cargo test test_name # Ejecutar pruebas que coincidan con el patrón
cargo test --lib # Solo pruebas unitarias
cargo test --test api_test # Solo pruebas de integración
cargo test --doc # Solo pruebas de documentación
cargo test --no-fail-fast # No detener al primer fallo
cargo test -- --ignored # Ejecutar pruebas ignoradas
Buenas Prácticas
HACER:
- Escribir pruebas PRIMERO (TDD)
- Usar módulos
#[cfg(test)]para pruebas unitarias - Probar comportamiento, no implementación
- Usar nombres de prueba descriptivos que expliquen el escenario
- Preferir
assert_eq!sobreassert!para mejores mensajes de error - Usar
?en pruebas que retornanResultpara salida de errores más limpia - Mantener las pruebas independientes — sin estado mutable compartido
NO HACER:
- Usar
#[should_panic]cuando se puede probarResult::is_err() - Mockear todo — preferir pruebas de integración cuando sea factible
- Ignorar pruebas inestables — corregirlas o ponerlas en cuarentena
- Usar
sleep()en pruebas — usar canales, barreras otokio::time::pause() - Omitir las pruebas de rutas de error
Integración con CI
# 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
Recuerda: Las pruebas son documentación. Muestran cómo debe usarse tu código. Escríbelas con claridad y mantenlas actualizadas.
Frequently asked questions about Rust Testing Patterns
Similar skills
Spring Boot Testing
Master testing techniques for Spring Boot 4 applications.
GitHub Issues
Manage GitHub issues efficiently with MCP tools.
Geofeed Tuner
Optimize your IP geolocation feeds in CSV format.
Batch Files
Master Windows batch scripting for automation and task management.
Adobe Illustrator Scripting
Automate your Illustrator workflows with ExtendScript.
Plugin Structure
Create and organize Claude Code plugins effectively.
