mocktail is a minimal crate for mocking HTTP and gRPC servers in Rust, with native support for streaming.
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
-
Add
mocktailtoCargo.tomlas a development dependency:[dev-dependencies] mocktail = "0.3.0" -
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_http("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(()) } -
For more examples, see examples in the
mocktail-testscrate.
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 {
/// Mock ID.
pub id: Uuid,
/// A set of request match conditions.
pub matchers: Vec<Arc<dyn Matcher>>,
/// A mock response.
pub response: Response,
/// Priority.
pub priority: u8, // defaults to 5 (more on this later)
/// Match counter.
pub match_count: AtomicUsize,
/// Limit on how many times this mock can be matched.
pub limit: Option<usize>,
}
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:
Mock::new(|when, then|...)to build a standalone mockMockSet::mock(|when, then|...)shorthand to build a mock and insert it into the mock setMockServer::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 customMatcherimplementations)
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();
})
head()
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.
Header
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_options() methods to build and insert mocks directly into it.
The server calls it's MockSet::match_by_request() 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.
HTTP
Use MockServer::new_http() to create a HTTP mock server.
gRPC
Use MockServer::new_grpc() to create a gRPC mock server. You can use tonic to connect to the gRPC service, e.g.
let server = MockServer::new_grpc("name");
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_http("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_http("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