../announcing-mocktail

Announcing mocktail: HTTP & gRPC server mocking for Rust

This is an announcement post for mocktail, a minimal crate for mocking HTTP and gRPC servers in Rust, with native support for streaming.

Motivation

At IBM Research, my team is building an AI platform service in Rust. This service calls out to several HTTP and gRPC services, a mix of unary and streaming methods. Our requirements are simple: to properly test our code, we need to mock these services, otherwise we have to deploy real services just for testing. That's no fun.

While there are some great mocking libraries in the Rust ecosystem such as httpmock, wiremock-rs, and stubr, none of them support gRPC or streaming.

I reviewed these crates to see if it would be feasible to contribute streaming and gRPC support, but it really did not seem like a good fit for their designs, so I decided to experiment with creating a new crate from the ground up.

Key requirements:

The result of this experiment is mocktail, which I am happy to share with the community, in case others have similar needs. I'll share additional technical details and learnings from this in future posts.

Example

A basic usage example:

use anyhow::Error;
use mocktail::prelude::*;
use http::StatusCode;

#[tokio::test]
async fn test_example() -> Result<(), Error> {
    // Create a mock set
    let mut mocks = MockSet::new();

    // Build a mock
    mocks.mock(|when, then| {
        when.post().path("/hello").text("world");
        then.text("hello world!");
    });
    // Shout out to httpmock for inspiring this 
    // closure-builder API design :)

    // Create and start a mock server
    let mut server = MockServer::new("example").with_mocks(mocks);
    server.start().await?;

    // Create a client
    let client = reqwest::Client::builder().build()?;

    // Send a request that matches the mock created above
    let response = client
        .post(server.url("/hello"))
        .body("world")
        .send()
        .await?;
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = response.text().await?;
    assert_eq!(body, "hello world!");

    // Send a request that doesn't match a mock
    let response = client
        .get(server.url("/nope"))
        .send()
        .await?;
    
    assert_eq!(response.status(), StatusCode::NOT_FOUND);

    // Mocks can also be registered to the server directly

    // Build a mock that will match the request above 
    // that returned 404
    server.mock(|when, then| {
        when.get().path("/nope");
        then.text("yep!");
    });

    // Send the request again, it should now match
    let response = client
        .get(server.url("/nope"))
        .send()
        .await?;
    
    assert_eq!(response.status(), StatusCode::OK);
    let body = response.text().await?;
    assert_eq!(body, "yep!");

    // Mocks can be cleared from the server, enabling server reuse
    server.mocks.clear();

    Ok(())
}

See the book for additional details (WIP), docs, and examples in the mocktail-tests crate for more.

This is an early stage alpha, subject to bugs and breaking changes.

Next Steps