| applyTo | **/*.rs |
|---|---|
| description | Rust test code authoring conventions |
Conventions for Rust test code. All conventions from rust.instructions.md apply, including naming, error handling, and module structure.
Place unit tests in #[cfg(test)] mod tests within the source file they exercise:
#[cfg(test)]
mod tests {
use super::*;
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};
#[test]
fn given_valid_input_parse_returns_config() {
let json = r#"{"endpoint": "http://127.0.0.1"}"#;
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 mock_server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(200).set_body_bytes(b"data"))
.mount(&mock_server)
.await;
let config = AppConfig {
endpoint: mock_server.uri(),
polling_interval_secs: 10,
};
let service = PollingService::new(config);
let result = service.fetch().await;
assert!(result.is_ok(), "fetch should succeed when the mock endpoint is available");
}
}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.
| Library | Usage |
|---|---|
mockall |
Preferred for trait-based mocking |
wiremock |
HTTP server mocking in async tests |
mockito |
Lightweight HTTP mocking for synchronous or async tests |
Keep unit tests network-isolated. Unit tests must not perform DNS resolution or connect to non-loopback addresses. For HTTP behavior, inject a loopback endpoint supplied by wiremock, mockito, or an equivalent local test double. Tests that require a deployed service belong in a separately controlled integration-test environment.
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"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: "http://127.0.0.1".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.
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());
}- 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.
Types referenced below (AppConfig, ServiceError, Result alias) are defined in rust.instructions.md.
#[cfg(test)]
mod tests {
use super::*;
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};
// Fixture helper — see Test Data Patterns
fn sample_config(endpoint: String) -> AppConfig {
AppConfig {
endpoint,
polling_interval_secs: 10,
}
}
#[test]
fn given_defaults_config_has_ten_second_interval() {
let config = sample_config("http://127.0.0.1".into());
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_api_returns_service_unavailable_fetch_error_contains_status() {
let mock_server = MockServer::start().await;
Mock::given(method("GET"))
.respond_with(ResponseTemplate::new(503))
.mount(&mock_server)
.await;
let config = sample_config(mock_server.uri());
let service = PollingService::new(config);
let error = service.fetch().await.expect_err("503 response should fail");
assert!(error.to_string().contains("HTTP 503"), "error should include status");
}
}