wiremock-rs

repository·main·Indexed 21 days ago

https://github.com/lukemathwalker/wiremock-rs

An HTTP mocking library for Rust designed for black-box testing of applications interacting with third-party APIs. It allows developers to simulate HTTP servers with customizable request matching, response templating, and expectations verification. Compatible with both async_std and tokio runtimes, wiremock provides a variety of built-in matchers for methods, paths, headers, and bodies, as well as support for scoped mocks and custom Match and Respond traits.

Tokens
10.4K
Snippets
37
Records
49
Agent score
72%

What's inside wiremock

  1. How request matching works in wiremock

    main

    Request matching is handled by the matchers module. wiremock provides several out-of-the-box matching strategies. You can extend this functionality by:

    1. Implementing the Match trait for your own types.
    2. Using Fn closures as matchers.
  2. How test isolation is achieved

    main

    Each MockServer instance is fully isolated. When you call MockServer::start(), it finds a random available port on your local machine.

    Best Practice: To prevent cross-test interference, do not share MockServer instances between tests. Instead, create a new MockServer within each individual test. When the MockServer goes out of scope, the background HTTP server is automatically shut down.

  3. How spying and expectations work

    main

    You can set expectations on the number of times a Mock is invoked using the .expect() method. This allows you to verify that a specific side-effect has occurred (or hasn't).

    Verification Lifecycle: Expectations are automatically verified during the shutdown of the MockServer instance at the end of your test. If an expectation is not met, the test will panic.

  4. Get started with wiremock

    main

    To use wiremock, start a MockServer, define a Mock with specific request matchers (like method and path), specify a ResponseTemplate, and then mount the mock onto the server. The MockServer provides a .uri() method to get the base URL for your HTTP client to probe.

    use wiremock::{MockServer, Mock, ResponseTemplate};
    use wiremock::matchers::{method, path};
    
    #[async_std::main]
    async fn main() {
        // Start a background HTTP server on a random local port
        let mock_server = MockServer::start().await;
    
        // Arrange the behaviour of the MockServer adding a Mock:
        // when it receives a GET request on '/hello' it will respond with a 200.
        Mock::given(method("GET"))
            .and(path("/hello"))
            .respond_with(ResponseTemplate::new(200))
            // Mounting the mock on the mock server - it's now effective!
            .mount(&mock_server)
            .await;
    
        // If we probe the MockServer using any HTTP client it behaves as expected.
        let status = reqwest::get(format!("{}/hello", &mock_server.uri()))
            .await
            .unwrap()
            .status();
        assert_eq!(status.as_u16(), 200);
    
        // If the request doesn't match any `Mock` mounted on our `MockServer` a 404 is returned.
        let status = reqwest::get(format!("{}/missing", &mock_server.uri()))
            .await
            .unwrap()
            .status();
        assert_eq!(status.as_u16(), 404);
    }
  5. How custom matchers work in wiremock

    main

    Wiremock uses the Match trait to define request matching criteria. You can use any of the provided out-of-the-box matchers, or implement your own by creating a type that implements the Match trait.

    Additionally, any closure with the signature Fn(&Request) -> bool (where Request is the incoming request object) automatically implements Match and can be passed directly where a matcher is expected.

    // Example of using a closure as a matcher
    Mock::given(|request: &Request| request.method == Method::GET)
        .respond_with(ResponseTemplate::new(200))
        .mount(&mock_server)
        .await;
  6. How `ResponseTemplate` works

    main

    A ResponseTemplate acts as a blueprint for the response a MockServer will return. It encapsulates the status code, headers, body, MIME type, and an optional delay.

    When a Mock is mounted to a MockServer using .respond_with(template), the server uses this blueprint to construct a full HTTP response whenever the request matches the mock's criteria. The template uses a builder-like pattern, allowing you to chain configuration methods (like insert_header or set_body_json) before passing it to the mock.

    use wiremock::{MockServer, Mock, ResponseTemplate};
    use wiremock::matchers::method;
    
    // 1. Create the template (the blueprint)
    let template = ResponseTemplate::new(200)
        .insert_header("Content-Type", "application/json")
        .set_body_json(vec![1, 2, 3]);
    
    // 2. Attach the template to a Mock
    Mock::given(method("GET"))
        .respond_with(template)
        .mount(&mock_server)
        .await;
  7. Use scoped mocks with register_as_scoped

    main

    Use register_as_scoped(mock) when you need a mock to exist only for the duration of a specific scope (e.g., inside a test helper function).

    This method returns a MockGuard. When the MockGuard is dropped:

    1. The MockServer automatically verifies that the expectations set on the scoped Mock were met.
    2. If expectations were not met, the program will panic.

    Debugging Tip: Because panics occurring in a Drop implementation may not show the exact line of code where the guard was dropped, use Mock::named("unique_name") to assign identifiers to your mocks. This identifier will appear in the panic message, making it easier to identify which scoped mock failed.

    use wiremock::{MockServer, Mock, ResponseTemplate};
    use wiremock::matchers::method;
    
    async fn my_test_helper(mock_server: &MockServer) {
        let mock = Mock::given(method("GET"))
            .respond_with(ResponseTemplate::new(200))
            .expect(1)
            .named("my_test_helper GET /"); // Named for better panic messages
        
        let _mock_guard = mock_server.register_as_scoped(mock).await;
    
        reqwest::get(&mock_server.uri())
            .await
            .unwrap();
    
        // _mock_guard is dropped here, expectations are verified!
    }
    
    #[async_std::main]
    async fn main() {
        let mock_server = MockServer::start().await;
        my_test_helper(&mock_server).await;
    }
  8. Runtime compatibility and efficiency

    main

    Runtime Compatibility

    wiremock is compatible with both async_std and tokio futures runtimes.

    Efficiency

    wiremock uses a background pool of mock servers to minimize connection overhead and startup time. This pooling is handled automatically and is designed to be transparent to the user.

  9. How request matching and responses work

    main

    Request Matching

    wiremock uses Matchers to determine if an incoming request should trigger a specific Mock. You can use built-in matchers from the matchers module or define custom ones using the Match trait or Fn closures.

    Responses

    Once a request is matched, you can specify a response using ResponseTemplate via the respond_with method. For more complex logic, you can implement the Respond trait to return different responses based on the matched Request.

    Spying and Expectations

    You can set expectations on the number of times a Mock is invoked using the expect method. These expectations are automatically verified when the MockServer instance is shut down (e.g., at the end of a test). If an expectation is not met, the test will panic.

  10. Create and mount a `Mock`

    main

    A Mock defines a set of matching conditions and a response. To make a Mock effective, it must be registered with a MockServer using one of the following methods:

    1. Global Mocks: Use Mock::mount(&server) or server.register(mock) to create a mock that remains active until the server is shut down.
    2. Scoped Mocks: Use Mock::mount_as_scoped(&server) to create a mock that is only active as long as the returned MockGuard is in scope. When the guard is dropped, wiremock verifies that the mock's expectations were met; if not, the server will panic.
    // Global mount
    Mock::given(method("GET"))
        .respond_with(ResponseTemplate::new(200))
        .mount(&mock_server)
        .await;
    
    // Scoped mount
    {
        let _guard = Mock::given(method("GET"))
            .respond_with(ResponseTemplate::new(200))
            .expect(1)
            .mount_as_scoped(&mock_server)
            .await;
        // Mock is active here
    }
    // Mock is dropped and expectations verified here