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
mocktail
toCargo.toml
as a development dependency:[dev-dependencies] mocktail = "0.2.4-alpha"
-
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(()) }
-
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:
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 customMatcher
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();
})
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_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