wiremock-rs: HTTP Mocking in Rust Integration Tests
wiremock-rs runs a real HTTP server in your test process. Your code under test makes actual HTTP calls — the mock server intercepts them, matches against your rules, and returns the responses you specify. No code changes required, no faking the HTTP client layer.
Key Takeaways
- MockServer starts a real HTTP server on a random port — no port conflicts
- Mock::given() chains matchers for method, path, headers, and body
- ResponseTemplate sets status, headers, and response body
- mount() registers a mock; mounted mocks are verified on drop
- Times::exactly(n) asserts call count at the end of the test
Why a real mock server
Some approaches mock the HTTP client (replacing reqwest::Client with a fake). That works but it tests the wrong layer — your code uses a different code path than production.
wiremock-rs starts an actual TCP listener. Your real HTTP client connects to it over loopback. The same serialization, timeout, retry, and connection-pooling code runs in tests as in production.
Setup
[dev-dependencies]
wiremock = "0.6"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde_json = "1"Basic mock
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path};
#[tokio::test]
async fn test_fetches_user() {
// Start mock server on a random port
let server = MockServer::start().await;
// Register a mock
Mock::given(method("GET"))
.and(path("/users/1"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(serde_json::json!({
"id": 1,
"name": "Alice"
})))
.mount(&server)
.await;
// Your code hits the mock server
let url = format!("{}/users/1", server.uri());
let response = reqwest::get(&url).await.unwrap();
assert_eq!(response.status().as_u16(), 200);
let body: serde_json::Value = response.json().await.unwrap();
assert_eq!(body["name"], "Alice");
}
// When `server` drops, wiremock verifies all mounted mocks were calledserver.uri() returns http://127.0.0.1:<port>. Pass this into your client or service under test wherever the base URL is configured.
Matching requests
Matchers compose with .and():
use wiremock::matchers::{method, path, header, body_json};
Mock::given(method("POST"))
.and(path("/orders"))
.and(header("Content-Type", "application/json"))
.and(header("Authorization", "Bearer test-token"))
.and(body_json(serde_json::json!({
"item": "widget",
"quantity": 3
})))
.respond_with(ResponseTemplate::new(201)
.set_body_json(serde_json::json!({ "order_id": "abc123" })))
.mount(&server)
.await;Available matchers:
| Matcher | Matches |
|---|---|
method("GET") |
HTTP method |
path("/foo") |
Exact path |
path_regex("^/users/[0-9]+$") |
Regex path |
header("name", "value") |
Exact header |
header_exists("X-Request-Id") |
Header presence |
query_param("page", "2") |
Query string |
body_string("text") |
Exact body string |
body_json(value) |
JSON body (exact match) |
body_partial_json(value) |
JSON body (subset match) |
Verifying call counts
By default, wiremock expects each mock to be called at least once. Use .expect() to be explicit:
use wiremock::Times;
// Must be called exactly once
Mock::given(method("POST"))
.and(path("/payments"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
// Must be called 3 times
Mock::given(method("GET"))
.and(path("/items"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!([])))
.expect(3)
.mount(&server)
.await;
// Must not be called
Mock::given(method("DELETE"))
.and(path("/data"))
.respond_with(ResponseTemplate::new(204))
.expect(0)
.mount(&server)
.await;When the MockServer drops at the end of the test, it verifies all expectations. Mismatches become test failures with a message that shows what was expected vs. what actually happened.
Simulating errors and slow responses
Test your client's error handling and timeouts:
use std::time::Duration;
// Server error
Mock::given(method("GET"))
.and(path("/flaky"))
.respond_with(ResponseTemplate::new(503)
.set_body_string("Service Unavailable"))
.mount(&server)
.await;
// Slow response (useful for testing timeouts)
Mock::given(method("GET"))
.and(path("/slow"))
.respond_with(ResponseTemplate::new(200)
.set_delay(Duration::from_secs(5)))
.mount(&server)
.await;
// Network-level error: use MockServer::builder().start_mock_error() for connection dropsStateful sequences
Use .up_to_n_times() to return different responses on repeated calls — simulating retry scenarios:
// First call fails
Mock::given(method("GET"))
.and(path("/resource"))
.respond_with(ResponseTemplate::new(500))
.up_to_n_times(2)
.mount(&server)
.await;
// Subsequent calls succeed
Mock::given(method("GET"))
.and(path("/resource"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(json!({ "data": "ok" })))
.mount(&server)
.await;Mocks are matched in registration order. The first-registered mock is tried first; when it's exhausted (.up_to_n_times hit), the next one takes over.
Testing a real service client
Here's a complete example testing an API client that wraps an external service:
struct WeatherClient {
base_url: String,
client: reqwest::Client,
}
impl WeatherClient {
fn new(base_url: &str) -> Self {
Self {
base_url: base_url.to_string(),
client: reqwest::Client::new(),
}
}
async fn get_temperature(&self, city: &str) -> anyhow::Result<f32> {
let url = format!("{}/weather?city={}", self.base_url, city);
let response = self.client.get(&url).send().await?;
if !response.status().is_success() {
anyhow::bail!("API error: {}", response.status());
}
let data: serde_json::Value = response.json().await?;
Ok(data["temperature"].as_f64().unwrap_or(0.0) as f32)
}
}
#[tokio::test]
async fn test_get_temperature_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/weather"))
.and(query_param("city", "Berlin"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(json!({ "temperature": 18.5, "unit": "celsius" })))
.expect(1)
.mount(&server)
.await;
let client = WeatherClient::new(&server.uri());
let temp = client.get_temperature("Berlin").await.unwrap();
assert!((temp - 18.5).abs() < 0.01);
}
#[tokio::test]
async fn test_get_temperature_api_error() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/weather"))
.respond_with(ResponseTemplate::new(503))
.mount(&server)
.await;
let client = WeatherClient::new(&server.uri());
let result = client.get_temperature("London").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("503"));
}Inspecting received requests
When a test fails and you want to see what requests actually arrived, use server.received_requests():
// After running your code...
let requests = server.received_requests().await.unwrap();
for req in &requests {
println!("Received: {} {}", req.method, req.url);
println!("Body: {:?}", std::str::from_utf8(&req.body));
}This is invaluable for debugging why a mock didn't match — you can see exactly what your client sent.
Integration with test fixtures
For tests that share the same mock setup, extract server initialization:
async fn setup_api_server() -> MockServer {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/health"))
.respond_with(ResponseTemplate::new(200).set_body_string("ok"))
.mount(&server)
.await;
server
}
#[tokio::test]
async fn test_health_check() {
let server = setup_api_server().await;
let client = MyClient::new(&server.uri());
assert!(client.is_healthy().await);
}Summary
wiremock-rs gives Rust tests a real HTTP mock server with minimal setup. Your production HTTP client runs unchanged — it connects to the mock over loopback just as it would connect to a real API. Request matching is composable, response templating handles JSON and delays, and automatic expectation verification on drop catches missing or extra calls. It's the right tool for any Rust service that makes outbound HTTP calls.