Sling Academy
Home/Rust/Testing External Services and APIs in Rust with Mock Servers

Testing External Services and APIs in Rust with Mock Servers

Last updated: January 06, 2025

When developing software in Rust, you might often find yourself needing to interact with external services and APIs. Testing these dependencies can be quite a challenge, especially when the external systems can be unpredictable or have usage limits. Fortunately, using mock servers, you can replicate external API conditions, allowing you to test your code thoroughly without making actual calls to the service.

Why Use Mock Servers?

Running tests against real external services can lead to unreliable test results. Some issues that arise from this approach include:

  • Network Fluctuation: Network issues can cause tests to fail inconsistently, making debugging difficult.
  • Service Downtime: The external service might be unavailable at times.
  • Rate Limits: Hitting API rate limits during testing can quickly become an issue.
  • Unpredictable Responses: Testing third-party services can yield unpredictable results based on their current state.

Mock servers enable you to define expected responses and simulate different scenarios, ensuring that your tests are stable and repeatable.

Setting Up a Mock Server in Rust

For creating mock servers in Rust, you can use the popular library wiremock. It allows you to set up a mock server that listens on a background thread and respond with predetermined payloads.

Step 1: Add Dependencies

First, you need to add the wiremock crate to your Cargo.toml:

[dev-dependencies]
wiremock = "0.5"

Step 2: Implementing the Mock Server

Let's set up a simple mock server that mimics a basic HTTP GET endpoint:

use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path};

#[tokio::test]
async fn test_external_service() {
    // Start a mock server
    let mock_server = MockServer::start().await;

    // Create the mock response
    let response = ResponseTemplate::new(200)
        .set_body_string("{ \"message\": \"Hello, World!\" }");

    // Setup mock server endpoint
    Mock::given(method("GET"))
        .and(path("/api/hello"))
        .respond_with(response)
        .mount(&mock_server)
        .await;

    // Here, you would add your actual function call that would use `mock_server.uri()`
    // as the base URL for testing, and then set your assertions
}

In this example, the mock server listens for GET requests on the endpoint /api/hello and shares a predefined JSON response. You can expand it by matching requests with different attributes or responding with various status codes.

Testing Against the Mock Server

After setting up the mock server, you can proceed to write tests for the client's expected behavior when interacting with this API. For example, if you wanted to test a function that calls this URL, you would replace the actual service's URL with mock_server.uri().

async fn fetch_message(base_url: &str) -> Result {
    let url = format!("{}/api/hello", base_url);
    let res = reqwest::get(&url).await?;
    Ok(res.text().await?)
}

#[tokio::test]
async fn test_fetch_message() {
    let mock_server = MockServer::start().await;

    let response = ResponseTemplate::new(200)
        .set_body_string("{ \"message\": \"Hello, World!\" }");

    Mock::given(method("GET"))
        .and(path("/api/hello"))
        .respond_with(response)
        .mount(&mock_server)
        .await;

    let message = fetch_message(&mock_server.uri()).await.unwrap();

    assert_eq!(message, "{ \"message\": \"Hello, World!\" }");
}

Handling Different Scenarios

Mock servers can handle a variety of scenarios beyond the simplest endpoints. For example, you might want to simulate HTTP error codes, introduce artificial delays, or more. Here is how you can simulate a 500 error:

let error_response = ResponseTemplate::new(500);

Mock::given(method("GET"))
    .and(path("/api/unstable"))
    .respond_with(error_response)
    .mount(&mock_server)
    .await;

Mock servers are instrumental for test-driving API client behavior without getting tangled in the complexities and limitations of the actual service. By simulating different scenarios dynamically, they allow the test suite to be more reliable and maintainable.

Next Article: Ensuring Thread Safety in Rust Tests by Checking for Data Races

Previous Article: Setting Up Continuous Integration for Rust Projects (GitHub Actions, etc.)

Series: Testing in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior