Mocking HTTP clients is essential for testing application logic without making actual HTTP requests. This approach speeds up tests, makes them more reliable, and removes dependencies on external services. For Rust applications using the reqwest
crate, several mocking libraries are available, with mockito
, wiremock
, and httpmock
being the most popular options.
Method 1: Using Mockito
Mockito is a lightweight library that creates a local mock server for testing HTTP interactions.
Setup
Add dependencies to your Cargo.toml
:
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
mockito = "1.2"
Basic GET Request Mock
#[cfg(test)]
mod tests {
use mockito::{mock, server_url};
use reqwest;
#[tokio::test]
async fn test_get_request() {
// Create a mock server response
let _m = mock("GET", "/api/users/1")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"id": 1, "name": "John Doe"}"#)
.create();
// Make request to mock server
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/api/users/1", server_url()))
.send()
.await
.unwrap();
// Verify response
assert_eq!(response.status(), 200);
let user: serde_json::Value = response.json().await.unwrap();
assert_eq!(user["name"], "John Doe");
}
}
Advanced Mocking with Request Validation
use mockito::{mock, Matcher};
#[tokio::test]
async fn test_post_with_validation() {
let _m = mock("POST", "/api/users")
.match_header("content-type", "application/json")
.match_body(Matcher::JsonString(r#"{"name":"Jane"}"#))
.with_status(201)
.with_header("location", "/api/users/2")
.with_body(r#"{"id": 2, "name": "Jane"}"#)
.create();
let client = reqwest::Client::new();
let response = client
.post(&format!("{}/api/users", server_url()))
.json(&serde_json::json!({"name": "Jane"}))
.send()
.await
.unwrap();
assert_eq!(response.status(), 201);
assert!(response.headers().get("location").is_some());
}
Method 2: Using Wiremock
Wiremock provides more sophisticated mocking capabilities with a fluent API.
Setup
[dev-dependencies]
wiremock = "0.5"
Example Usage
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path, header, body_json};
#[tokio::test]
async fn test_with_wiremock() {
// Start mock server
let mock_server = MockServer::start().await;
// Configure mock response
Mock::given(method("GET"))
.and(path("/api/weather"))
.and(header("authorization", "Bearer token123"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(serde_json::json!({
"temperature": 22.5,
"condition": "sunny"
})))
.mount(&mock_server)
.await;
// Test your client
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/api/weather", mock_server.uri()))
.header("authorization", "Bearer token123")
.send()
.await
.unwrap();
assert_eq!(response.status(), 200);
let weather: serde_json::Value = response.json().await.unwrap();
assert_eq!(weather["temperature"], 22.5);
}
Method 3: Using HTTPMock
HTTPMock offers a simple yet powerful API for HTTP mocking.
Setup
[dev-dependencies]
httpmock = "0.6"
Example Usage
use httpmock::prelude::*;
#[tokio::test]
async fn test_with_httpmock() {
// Start mock server
let server = MockServer::start();
// Create mock
let mock = server.mock(|when, then| {
when.method(GET)
.path("/api/status")
.query_param("version", "v1");
then.status(200)
.header("content-type", "application/json")
.json_body(serde_json::json!({"status": "healthy"}));
});
// Test your client
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/api/status?version=v1", server.base_url()))
.send()
.await
.unwrap();
assert_eq!(response.status(), 200);
mock.assert(); // Verify the mock was called
}
Testing Error Scenarios
Mock libraries excel at testing error conditions:
#[tokio::test]
async fn test_network_timeout() {
let _m = mock("GET", "/slow-endpoint")
.with_status(200)
.with_body("response")
.with_delay(std::time::Duration::from_secs(10)) // Simulate slow response
.create();
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(1))
.build()
.unwrap();
let result = client
.get(&format!("{}/slow-endpoint", server_url()))
.send()
.await;
assert!(result.is_err()); // Should timeout
}
#[tokio::test]
async fn test_server_error() {
let _m = mock("GET", "/api/data")
.with_status(500)
.with_body("Internal Server Error")
.create();
let client = reqwest::Client::new();
let response = client
.get(&format!("{}/api/data", server_url()))
.send()
.await
.unwrap();
assert_eq!(response.status(), 500);
}
Dependency Injection Pattern
For better testability, consider using dependency injection with your HTTP client:
#[derive(Clone)]
pub struct ApiClient {
client: reqwest::Client,
base_url: String,
}
impl ApiClient {
pub fn new(base_url: String) -> Self {
Self {
client: reqwest::Client::new(),
base_url,
}
}
pub async fn get_user(&self, id: u64) -> Result<User, reqwest::Error> {
let response = self.client
.get(&format!("{}/users/{}", self.base_url, id))
.send()
.await?;
response.json().await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_api_client() {
let _m = mock("GET", "/users/1")
.with_status(200)
.with_body(r#"{"id": 1, "name": "Test User"}"#)
.create();
// Inject mock server URL
let client = ApiClient::new(server_url());
let user = client.get_user(1).await.unwrap();
assert_eq!(user.name, "Test User");
}
}
Best Practices
- Use dev-dependencies: Keep mocking libraries in
[dev-dependencies]
to avoid including them in production builds - Test both success and failure cases: Mock various HTTP status codes and network conditions
- Validate request details: Use matchers to ensure your client sends correct headers, body content, and query parameters
- Clean up mocks: Most libraries automatically clean up, but be aware of mock lifecycle
- Combine with integration tests: Unit tests with mocks should complement integration tests against real services
Choosing the Right Library
- mockito: Lightweight, good for simple scenarios
- wiremock: Feature-rich, excellent for complex mocking needs
- httpmock: Good balance of simplicity and features, actively maintained
Choose based on your specific requirements for request matching, response templating, and API complexity.