How can I mock Reqwest clients for testing purposes?

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

  1. Use dev-dependencies: Keep mocking libraries in [dev-dependencies] to avoid including them in production builds
  2. Test both success and failure cases: Mock various HTTP status codes and network conditions
  3. Validate request details: Use matchers to ensure your client sends correct headers, body content, and query parameters
  4. Clean up mocks: Most libraries automatically clean up, but be aware of mock lifecycle
  5. 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.

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon