default-monochrome

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

Crates.io Documentation Crates.io

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

Features

  • Mocks HTTP and gRPC servers
  • Mocks defined in Rust using a simple, ergonomic API
  • Provides first-class support for streaming
  • Supports gRPC unary, client-streaming, server-streaming, and bidirectional-streaming methods
  • Match requests to mock responses using built-in matchers or custom matchers
  • Fully asynchronous

This user guide is a WIP DRAFT.

Getting Started

  1. Add mocktail to Cargo.toml as a development dependency:

    [dev-dependencies]
    mocktail = "0.2.4-alpha"
    
  2. Basic usage example:

    use mocktail::prelude::*;
    
    #[tokio::test]
    async fn test_example() -> Result<(), Box<dyn std::error::Error>> {
        // Create a mock set
        let mut mocks = MockSet::new();
    
        // Build a mock that returns a "hello world!" response
        // to POST requests to the /hello endpoint with the text "world"
        // in the body.
        mocks.mock(|when, then| {
            when.post().path("/hello").text("world");
            then.text("hello world!");
        });
    
        // 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().http2_prior_knowledge().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(), http::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(), http::StatusCode::NOT_FOUND);
    
        // Mocks can also be registered to the server directly
        // Register 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(), http::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(())
    }
  3. For more examples, see examples in the mocktail-tests crate.

Concepts

Mock Builder

"Mock builder" refers to a closure with When and Then parameters used to build the request match conditions and response, respectively.

    // A mock that returns the text "yo!" to any request
    let mock = Mock::new(|when, then| {
        when.any(); // builds a set of request match conditions
        then.text("yo!"); // builds the response to return when conditions are matched
    })

Together, they build a Mock, which consists of a set of request match conditions, a response, and a priority:

pub struct Mock {
    /// A set of request match conditions.
    pub matchers: Vec<Box<dyn Matcher>>,
    /// A mock response.
    pub response: Response,
    /// Priority.
    pub priority: u8, // defaults to 5 (more on this later)
}

Since when and then are just variables of types When and Then, you can name them however you'd like, e.g. the following also works.

    // A mock that returns the text "index" to get requests on the / endpoint
    let mock = Mock::new(|req, res| {
        req.get().path("/");
        res.text("index");
    })

We experimented with several different APIs and found this closure-builder pattern to feel the most ergonomic and nice to use.

The mock builder closure is exposed via 3 methods, allowing flexible usage patterns:

  1. Mock::new(|when, then|...) to build a standalone mock
  2. MockSet::mock(|when, then|...) shorthand to build a mock and insert it into the mock set
  3. MockServer::mock(|when, then|...) shorthand to build a mock and insert it into the server's mock set

When

When is a builder used to build request match conditions.

Method methods:

  • method() (primary)
  • get()
  • post()
  • put()
  • head()
  • delete()

Path methods:

  • path()
  • path_prefix()

Body methods:

  • body() (primary)
  • empty()
  • bytes()
  • bytes_stream()
  • text()
  • text_stream()
  • json()
  • json_lines_stream()
  • pb()
  • pb_stream()

Header methods:

  • headers()
  • headers_exact()
  • header()
  • header_exists()

Query Param methods:

  • query_params()
  • query_param()
  • query_param_exists()

Other methods:

  • any()
  • matcher() (for custom Matcher implementations)

Then

Then is a builder used to build responses.

Body methods:

  • body() (primary)
  • empty()
  • bytes()
  • bytes_stream()
  • text()
  • text_stream()
  • json()
  • json_lines_stream()
  • pb()
  • pb_stream()

Headers method:

  • headers()

Status methods:

  • status() (primary)
  • message()
  • error()
  • ok()
  • bad_request()
  • unauthorized()
  • forbidden()
  • not_found()
  • unsupported_media_type()
  • unprocessable_content()
  • internal_server_error()
  • not_implemented()
  • bad_gateway()
  • service_unavailable()
  • gateway_timeout()

Matchers

Matcher is a trait used to implement logic for matching a request to a mock. A mock consists of a set of matchers that must all evaluate true for a given request to be considered a match.

pub trait Matcher: std::fmt::Debug + Send + Sync + 'static {
    /// Matcher name.
    fn name(&self) -> &str;
    /// Evaluates a match condition.
    fn matches(&self, req: &Request) -> bool;
}

Several matchers are provided out of the box for common use cases:

  • MethodMatcher
  • PathMatcher
  • PathPrefixMatcher
  • BodyMatcher
  • HeadersMatcher
  • HeadersExactMatcher
  • HeaderMatcher
  • HeaderExistsMatcher
  • QueryParamsMatcher
  • QueryParamMatcher
  • AnyMatcher

Matcher types are not used directly; When has methods corresponding to all matchers plus additional convenience methods for body type variants, method variants, etc.

We are still expanding the list of matchers and welcome PRs to implement matchers for common use cases.

Custom matchers can be implemented with the Matcher trait. When::matcher() can be used to plug custom Matcher implementations.

Method

Method

Matches a request by HTTP method.

When methods:

method(method) (primary)

HTTP method.

get()

HTTP GET method.

Example:

let mock = Mock::new(|when, then| {
    when.get();
    then.ok();
})

post()

HTTP POST method.

Example:

let mock = Mock::new(|when, then| {
    when.post();
    then.ok();
})

put()

HTTP PUT method.

Example:

let mock = Mock::new(|when, then| {
    when.put();
    then.ok();
})

HTTP HEAD method.

Example:

let mock = Mock::new(|when, then| {
    when.head();
    then.ok();
})

delete()

HTTP DELETE method.

Example:

let mock = Mock::new(|when, then| {
    when.delete();
    then.ok();
})

Path

Path

Matches a request by path.

When method:

path(path)

Path.

Example:

let mock = Mock::new(|when, then| {
    when.path("/path");
    then.ok();
})

Path Prefix

Matches a request by path prefix. Returns true if the request path starts with prefix.

When method:

path_prefix(prefix)

Path prefix.

Example:

let mock = Mock::new(|when, then| {
    when.path_prefix("/p");
    then.ok();
})

Body

Body

Matches a request by body.

When methods:

body(body) (primary)

empty()

An empty body.

Example:

let mock = Mock::new(|when, then| {
    when.empty();
    then.ok();
})

bytes(body)

A raw bytes body. body is a type implementing Into<Bytes>.

let mock = Mock::new(|when, then| {
    when.bytes("hello".as_bytes());
    then.ok();
})

bytes_stream(messages)

A raw bytes streaming body. messages is an iterator of messages implementing Into<Bytes>.

let mock = Mock::new(|when, then| {
    when.bytes_stream([
        "msg1".as_bytes(), 
        "msg2".as_bytes(), 
        "msg3".as_bytes(),
    ]);
    then.ok();
})

text(body)

A text body. body is a type implementing Into<String>.

let mock = Mock::new(|when, then| {
    when.text("hello");
    then.ok();
})

text_stream(messages)

A text streaming body. messages is an iterator of messages implementing Into<String>.

let mock = Mock::new(|when, then| {
    when.text_stream([
        "msg1", 
        "msg2", 
        "msg3"
    ]);
    then.ok();
})

json(body)

A json body. body is a type implementing serde::Serialize.

use serde_json::json;
let mock = Mock::new(|when, then| {
    when.json(json!({"message": "hello"}));
    then.ok();
})

json_lines_stream(messages)

A newline delimited json streaming body. messages is an iterator of messages implementing serde::Serialize.

use serde_json::json;
let mock = Mock::new(|when, then| {
    when.json_lines_stream([
        json!({"message": "msg1"}), 
        json!({"message": "msg2"}), 
        json!({"message": "msg3"}),
    ]);
    then.ok();
})

pb(body)

A protobuf body. body is a prost-generated type implementing prost::Message.

let mock = Mock::new(|when, then| {
    when.pb(ExampleMessage { message: "msg" });
    then.ok();
})

pb_stream(messages)

A protobuf streaming body. messages is an iterator of messages implementing prost::Message.

let mock = Mock::new(|when, then| {
    when.pb_stream([
        ExampleMessage { message: "msg1" }, 
        ExampleMessage { message: "msg2" }, 
        ExampleMessage { message: "msg3" },
    ]);
    then.ok();
})

Headers

Headers

Matches a request by headers. Returns true if the request headers are a superset of the headers, i.e. contains at least all of the headers.

When method:

headers(headers)

Headers. headers is an iterator of name-value pairs.

Headers Exact

Matches a request by exact headers. Returns true if the request headers are equal to the headers.

When method:

headers_exact(headers)

Exact headers. headers is an iterator of name-value pairs.

Matches a request by header. Returns true if the request contains a header equal to the header.

When method:

header(name, value)

Header. name and value are types implementing Into<String>.

Header Exists

Matches a request by header exists. Returns true if the request contains a header with the header name.

When method:

header_exists(name)

Header exists. name is a type implementing Into<String>.

Query Params

Query Params

Matches a request by query params. Returns true if the request query params are equal to the query params.

When method:

query_params(params)

Query params. params is an iterator of key-value pairs.

Query Param

Matches a request by query param. Returns true if the request contains a query param equal to the query param.

When method:

query_param(key, value)

Query param. key and value are types implementing Into<String>.

Query Param Exists

Matches a request by query param exists. Returns true if the request contains a query param with the query key.

When method:

query_param_exists(key)

Query param exists. key is a type implementing Into<String>.

Any

Any

Matches any request. Should not be combined with other matchers.

When method

any()

Matches any request.

Example:

let mock = Mock::new(|when, then| {
    when.any();
    then.ok();
})

Custom

Custom

Matches a request by a custom Matcher implementation.

When method:

matcher(matcher)

Custom matcher. matcher is type implementing Matcher.

Mock Set

A mock set is simply a set of mocks for a mock server. It is implemented as a newtype wrapping Vec<Mock>.

It keeps mocks sorted by priority and ensures that there are no duplicates. It has shorthand MockSet::mock() and MockSet::mock_with_priority() methods to build and insert mocks directly into it.

The server calls it's MockSet::match_to_response() method to match incoming requests to mock responses.

Priority

Mock Server

The mock server is a simple, lightweight HTTP server designed for serving mocks. It has 2 service implementations: HttpMockService and GrpcMockService. The server supports HTTP/1 and HTTP/2.

gRPC

By default, the HttpMockService is used for serving regular HTTP mocks. For gRPC, set the grpc() option on the server to enable the GrpcMockService. You can use tonic to connect to the gRPC service.

    let server = MockServer::new().grpc();
    let url = format!("http://0.0.0.0:{}", server.port().unwrap());
    let channel = tonic::Channel::from_shared(url)?
        .connect()
        .await?;
    // Some client generated with tonic-build
    let mut client = ExampleClient::new(channel);

TLS

TLS support is not yet implemented, but it will be added in the near future.

Defining Mocks

Mocks are defined in Rust using a simple, ergonomic builder-like API.

You can define your mocks first, then create a mock server with your mock set:

    // Create a mock set
    let mut mocks = MockSet::new();
    // Build and insert a mock
    mocks.mock(|when, then| {
        when.get().path("/health");
        then.text("healthy!");
    });
    // Alternatively, Mock::new() and mocks.insert(mock)

    // Create mock server with the mock set
    let mut server = MockServer::new("example").with_mocks(mocks);
    server.run().await?;

Or, you can create a mock server with a default empty mock set and register mocks directly to the server:

    // Create mock server
    let mut server = MockServer::new("example");
    server.run().await?;

    // Build and insert a mock to the server's mock set
    server.mock(|when, then| {
        when.get().path("/health");
        then.text("healthy!");
    });
    // Alternatively, use Mock::new() and server.mocks.insert(mock)

Example: HTTP Simple

TODO

Example: HTTP Streaming (ndjson)

TODO

Example: HTTP Streaming (SSE)

TODO

Example: gRPC Unary

TODO

Example: gRPC Client Streaming

TODO

Example: gRPC Server Streaming

TODO

Example: gRPC Bidi Streaming

TODO

Example: Custom Matcher

FAQ

CHANGELOG

CONTRIBUTING