Rust Test Instructions
June 9, 2026 · View on GitHub
Conventions for Rust test code. All conventions from rust.instructions.md apply, including naming, error handling, and module structure.
Test Module Placement
Place unit tests in #[cfg(test)] mod tests within the source file they exercise:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn given_valid_input_parse_returns_config() {
let json = r#"{"endpoint": "https://example.com"}"#;
let config: AppConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.polling_interval_secs, 10);
}
#[tokio::test]
async fn when_endpoint_available_fetch_returns_data() {
let service = PollingService::new(AppConfig::from_env());
let result = service.fetch().await;
assert!(result.is_ok(), "fetch should succeed when endpoint is available");
}
}
Test Naming
Test method format: given_context_when_action_then_expected or descriptive snake_case that reads as a behavior statement.
given_valid_input_parse_returns_config
when_endpoint_unavailable_send_returns_error
parses_empty_payload_as_default
Prefer one assertion per test. Related assertions validating the same behavior are acceptable.
Mocking Libraries
| Library | Usage |
|---|---|
mockall | Preferred for trait-based mocking |
wiremock | HTTP server mocking in async tests |
mockito | Lightweight HTTP mocking for synchronous or async tests |
Use mockall to generate mock implementations from traits via #[automock]:
use mockall::automock;
// Application types — defined in your crate (see rust.instructions.md)
pub struct Item {
pub id: String,
}
// Uses the module-scoped Result alias from rust.instructions.md
#[automock]
pub trait Repository: Send + Sync {
fn find_by_id(&self, id: &str) -> Result<Option<Item>>;
}
pub struct ItemService {
repo: Box<dyn Repository>,
}
impl ItemService {
pub fn new(repo: Box<dyn Repository>) -> Self {
Self { repo }
}
pub fn get(&self, id: &str) -> Result<Option<Item>> {
self.repo.find_by_id(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::*;
#[test]
fn given_existing_item_service_returns_it() {
let mut mock = MockRepository::new();
mock.expect_find_by_id()
.with(eq("42"))
.returning(|_| Ok(Some(Item { id: "42".into() })));
let service = ItemService::new(Box::new(mock));
let result = service.get("42").unwrap();
assert_eq!(result.unwrap().id, "42");
}
}
Use wiremock to mock HTTP servers in async tests:
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::method;
#[tokio::test]
async fn when_api_returns_ok_fetch_succeeds() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(200).set_body_string(r#"{"id": "1"}"#))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let response = client.get(mock_server.uri()).send().await.unwrap();
assert_eq!(response.status(), 200);
}
Add test dependencies to [dev-dependencies] in Cargo.toml:
[dev-dependencies]
mockall = "0.13"
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["macros", "rt"] }
wiremock = "0.6"
Test Data Patterns
Use builder functions or fixture helpers for test data rather than repeating inline construction:
#[cfg(test)]
mod tests {
use super::*;
fn sample_config() -> AppConfig {
AppConfig {
endpoint: "https://example.com".into(),
polling_interval_secs: 10,
}
}
#[test]
fn given_custom_interval_config_uses_override() {
let config = AppConfig {
polling_interval_secs: 30,
..sample_config()
};
assert_eq!(config.polling_interval_secs, 30);
}
}
Inline construction is acceptable for simple one-field tests where a builder adds no clarity.
Integration Tests
Place integration tests in the tests/ directory at the crate root. Each file in tests/ compiles as a separate crate with access to the library's public API only:
// tests/polling_integration.rs
use my_service::AppConfig;
#[tokio::test]
async fn given_valid_config_service_starts() {
let config = AppConfig {
endpoint: "https://example.com".into(),
polling_interval_secs: 1,
};
assert!(!config.endpoint.is_empty());
}
Test Conventions
- Use
#[tokio::test]for async tests. - Prefer assertion messages that explain intent:
assert!(result.is_ok(), "should parse valid JSON"). - Use builder functions or fixture helpers for test data rather than repeating inline construction.
- Place integration tests in the
tests/directory at the crate root.
Complete Example
Types referenced below (AppConfig, ServiceError, Result alias) are defined in rust.instructions.md.
#[cfg(test)]
mod tests {
use super::*;
// Fixture helper — see Test Data Patterns
fn sample_config() -> AppConfig {
AppConfig {
endpoint: "https://example.com".into(),
polling_interval_secs: 10,
}
}
#[test]
fn given_defaults_config_has_ten_second_interval() {
let config = sample_config();
assert_eq!(config.polling_interval_secs, 10);
}
#[test]
fn service_error_not_found_formats_message() {
let err = ServiceError::not_found("item 42");
assert_eq!(err.to_string(), "Not found: item 42");
}
#[tokio::test]
async fn when_fetch_fails_error_contains_status() {
let config = sample_config();
let service = PollingService::new(config);
let result = service.fetch().await;
assert!(result.is_err(), "fetch should fail with unreachable endpoint");
}
}