Crate mockito

Source
Expand description

Mockito is a library for generating and delivering HTTP mocks in Rust. You can use it for integration testing or offline work. Mockito runs a local pool of HTTP servers which create, deliver and remove the mocks.

§Features

  • Supports HTTP1/2
  • Runs your tests in parallel
  • Comes with a wide range of request matchers (Regex, JSON, query parameters etc.)
  • Checks that a mock was called (spy)
  • Mocks multiple hosts at the same time
  • Exposes sync and async interfaces
  • Prints out a colored diff of the last unmatched request in case of errors
  • Simple, intuitive API
  • An awesome logo

§Getting Started

Add mockito to your Cargo.toml and start mocking:

#[cfg(test)]
mod tests {
  #[test]
  fn test_something() {
    // Request a new server from the pool
    let mut server = mockito::Server::new();

    // Use one of these addresses to configure your client
    let host = server.host_with_port();
    let url = server.url();

    // Create a mock
    let mock = server.mock("GET", "/hello")
      .with_status(201)
      .with_header("content-type", "text/plain")
      .with_header("x-api-key", "1234")
      .with_body("world")
      .create();

    // Any calls to GET /hello beyond this line will respond with 201, the
    // `content-type: text/plain` header and the body "world".

    // You can use `Mock::assert` to verify that your mock was called
    // mock.assert();
  }
}

If Mock::assert fails, a colored diff of the last unmatched request is displayed:

colored-diff.png

Use matchers to handle requests to the same endpoint in a different way:

#[cfg(test)]
mod tests {
  #[test]
  fn test_something() {
    let mut server = mockito::Server::new();

    server.mock("GET", "/greetings")
      .match_header("content-type", "application/json")
      .match_body(mockito::Matcher::PartialJsonString(
          "{\"greeting\": \"hello\"}".to_string(),
      ))
      .with_body("hello json")
      .create();

    server.mock("GET", "/greetings")
      .match_header("content-type", "application/text")
      .match_body(mockito::Matcher::Regex("greeting=hello".to_string()))
      .with_body("hello text")
      .create();
  }
}

Start multiple servers to simulate requests to different hosts:

#[cfg(test)]
mod tests {
  #[test]
  fn test_something() {
    let mut twitter = mockito::Server::new();
    let mut github = mockito::Server::new();

    // These mocks will be available at `twitter.url()`
    let twitter_mock = twitter.mock("GET", "/api").create();

    // These mocks will be available at `github.url()`
    let github_mock = github.mock("GET", "/api").create();
  }
}

Write async tests (make sure to use the _async methods!):

#[cfg(test)]
mod tests {
  #[tokio::test]
  async fn test_something() {
    let mut server = Server::new_async().await;
    let m1 = server.mock("GET", "/a").with_body("aaa").create_async().await;
    let m2 = server.mock("GET", "/b").with_body("bbb").create_async().await;

    let (m1, m2) = futures::join!(m1, m2);

    // You can use `Mock::assert_async` to verify that your mock was called
    // m1.assert_async().await;
    // m2.assert_async().await;
  }
}

Start a stand-alone server on a dedicated port:

fn main() {
    let opts = mockito::ServerOpts {
        host: "0.0.0.0",
        port: 1234,
        ..Default::default()
    };
    let mut server = mockito::Server::new_with_opts(opts);

    let _m = server.mock("GET", "/").with_body("hello world").create();

    // loop {}
}

§Lifetime

A mock is available only throughout the lifetime of the server. Once the server goes out of scope, all mocks defined on that server are removed:

let address;

{
    let mut s = mockito::Server::new();
    address = s.host_with_port();

    s.mock("GET", "/").with_body("hi").create();

    // Requests to `address` will be responded with "hi" til here
}

// Requests to `address` will fail as of this point

You can remove individual mocks earlier by calling Mock::remove.

§Async

Mockito comes with both a sync and an async interface.

In order to write async tests, you’ll need to use the *_async methods:

…otherwise your tests will not compile, and you’ll see the following error:

Cannot block the current thread from within a runtime.
This happens because a function attempted to block the current thread while the thread is being used to drive asynchronous tasks.

§Configuring the server

When calling Server::new(), a mock server with default options is returned from the server pool. This should suffice for most use cases.

If you’d like to bypass the server pool or configure the server in a different way, you can use Server::new_with_opts. The following options are available:

  • host: allows setting the host (defaults to 127.0.0.1)
  • port: allows setting the port (defaults to a randomly assigned free port)
  • assert_on_drop: automatically call Mock::assert() before dropping a mock (defaults to false)
let opts = mockito::ServerOpts { assert_on_drop: true, ..Default::default() };
let server = mockito::Server::new_with_opts(opts);

§Matchers

Mockito can match your request by method, path, query, headers or body.

Various matchers are provided by the Matcher type: exact (string, binary, JSON), partial (regular expressions, JSON), any or missing. The following guide will walk you through the most common matchers. Check the Matcher documentation for all the rest.

§Matching by path and query

By default, the request path and query is compared by its exact value:

§Example

let mut s = mockito::Server::new();

// Matches only calls to GET /hello
s.mock("GET", "/hello").create();

// Matches only calls to GET /hello?world=1
s.mock("GET", "/hello?world=1").create();

You can also match the path partially, by using a regular expression:

§Example

let mut s = mockito::Server::new();

// Will match calls to GET /hello/1 and GET /hello/2
s.mock("GET",
    mockito::Matcher::Regex(r"^/hello/(1|2)$".to_string())
  ).create();

Or you can catch all requests, by using the Matcher::Any variant:

§Example

let mut s = mockito::Server::new();

// Will match any GET request
s.mock("GET", mockito::Matcher::Any).create();

§Matching by query

You can match the query part by using the Mock::match_query function together with the various matchers, most notably Matcher::UrlEncoded:

§Example

let mut s = mockito::Server::new();

// This will match requests containing the URL-encoded
// query parameter `greeting=good%20day`
s.mock("GET", "/test")
  .match_query(mockito::Matcher::UrlEncoded("greeting".into(), "good day".into()))
  .create();

// This will match requests containing the URL-encoded
// query parameters `hello=world` and `greeting=good%20day`
s.mock("GET", "/test")
  .match_query(mockito::Matcher::AllOf(vec![
    mockito::Matcher::UrlEncoded("hello".into(), "world".into()),
    mockito::Matcher::UrlEncoded("greeting".into(), "good day".into())
  ]))
  .create();

// You can achieve similar results with the regex matcher
s.mock("GET", "/test")
  .match_query(mockito::Matcher::Regex("hello=world".into()))
  .create();

Note that the key/value arguments for Matcher::UrlEncoded should be left in plain (unencoded) format.

You can also specify the query as part of the path argument in a mock call, in which case an exact match will be performed:

§Example

let mut s = mockito::Server::new();

// This will perform a full match against the query part
s.mock("GET", "/test?hello=world").create();

If you’d like to ignore the query entirely, use the Matcher::Any variant:

§Example

let mut s = mockito::Server::new();

// This will match requests to GET /test with any query
s.mock("GET", "/test").match_query(mockito::Matcher::Any).create();

§Matching by header

By default, headers are compared by their exact value. The header name letter case is ignored though.

§Example

let mut s = mockito::Server::new();

s.mock("GET", "/hello")
  .match_header("content-type", "application/json")
  .with_body(r#"{"hello": "world"}"#)
  .create();

s.mock("GET", "/hello")
  .match_header("content-type", "text/plain")
  .with_body("world")
  .create();

// JSON requests to GET /hello will respond with JSON, while plain requests
// will respond with text.

You can also match a header value with a regular expressions, by using the Matcher::Regex matcher:

§Example

let mut s = mockito::Server::new();

s.mock("GET", "/hello")
  .match_header("content-type", mockito::Matcher::Regex(r".*json.*".to_string()))
  .with_body(r#"{"hello": "world"}"#)
  .create();

Or you can match a header only by its field name, by setting the Mock::match_header value to Matcher::Any.

§Example

let mut s = mockito::Server::new();

s.mock("GET", "/hello")
 .match_header("content-type", mockito::Matcher::Any)
 .with_body("something")
 .create();

// Requests containing any content-type header value will be mocked.
// Requests not containing this header will return `501 Not Implemented`.

You can mock requests that should be missing a particular header field, by setting the Mock::match_header value to Matcher::Missing.

§Example

let mut s = mockito::Server::new();

s.mock("GET", "/hello")
  .match_header("authorization", mockito::Matcher::Missing)
  .with_body("no authorization header")
  .create();

// Requests without the authorization header will be matched.
// Requests containing the authorization header will return `501 Mock Not Found`.

§Matching by body

You can match a request by its body by using the Mock::match_body method. By default, the request body is ignored, similar to passing the Matcher::Any argument to the Mock::match_body method.

You can match a body by an exact value:

§Example

let mut s = mockito::Server::new();

// Will match requests to POST / whenever the request body is "hello"
s.mock("POST", "/").match_body("hello").create();

Or you can match the body by using a regular expression:

§Example

let mut s = mockito::Server::new();

// Will match requests to POST / whenever the request body *contains* the word "hello" (e.g. "hello world")
s.mock("POST", "/").match_body(
    mockito::Matcher::Regex("hello".to_string())
  ).create();

Or you can match the body using a JSON object:

§Example

#[macro_use]
extern crate serde_json;

let mut s = mockito::Server::new();
// Will match requests to POST / whenever the request body matches the json object
s.mock("POST", "/").match_body(mockito::Matcher::Json(json!({"hello": "world"}))).create();

If serde_json::json! is not exposed, you can use Matcher::JsonString the same way, but by passing a String to the matcher:

let mut s = mockito::Server::new();

// Will match requests to POST / whenever the request body matches the json object
s.mock("POST", "/")
    .match_body(
       mockito::Matcher::JsonString(r#"{"hello": "world"}"#.to_string())
    )
    .create();

§The AnyOf matcher

The Matcher::AnyOf construct takes a vector of matchers as arguments and will be enabled if at least one of the provided matchers matches the request.

§Example

let mut s = mockito::Server::new();

// Will match requests to POST / whenever the request body is either `hello=world` or `{"hello":"world"}`
s.mock("POST", "/")
    .match_body(
        mockito::Matcher::AnyOf(vec![
            mockito::Matcher::Exact("hello=world".to_string()),
            mockito::Matcher::JsonString(r#"{"hello": "world"}"#.to_string()),
        ])
     )
    .create();

§The AllOf matcher

The Matcher::AllOf construct takes a vector of matchers as arguments and will be enabled if all the provided matchers match the request.

§Example

let mut s = mockito::Server::new();

// Will match requests to POST / whenever the request body contains both `hello` and `world`
s.mock("POST", "/")
    .match_body(
        mockito::Matcher::AllOf(vec![
            mockito::Matcher::Regex("hello".to_string()),
            mockito::Matcher::Regex("world".to_string()),
        ])
     )
    .create();

§Custom matchers

If you need a more custom matcher, you can use the Mock::match_request function, which takes a closure and exposes the Request object as an argument. The closure should return a boolean value.

§Example

use mockito::Matcher;

let mut s = mockito::Server::new();

// This will match requests that have the x-test header set
// and contain the word "hello" inside the body
s.mock("GET", "/")
    .match_request(|request| {
        request.has_header("x-test") &&
            request.utf8_lossy_body().unwrap().contains("hello")
    })
    .create();

§Asserts

You can use the Mock::assert method to assert that a mock was called. In other words, Mock#assert can validate that your code performed the expected HTTP request.

By default, the method expects only one request to your mock.

§Example

use std::net::TcpStream;
use std::io::{Read, Write};

let mut s = mockito::Server::new();
let mock = s.mock("GET", "/hello").create();

{
    // Place a request
    let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
    stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    stream.flush().unwrap();
}

mock.assert();

When several mocks can match a request, Mockito applies the first one that still expects requests. You can use this behaviour to provide different responses for subsequent requests to the same endpoint.

§Example

use std::net::TcpStream;
use std::io::{Read, Write};

let mut s = mockito::Server::new();
let english_hello_mock = s.mock("GET", "/hello").with_body("good bye").create();
let french_hello_mock = s.mock("GET", "/hello").with_body("au revoir").create();

{
    // Place a request to GET /hello
    let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
    stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    stream.flush().unwrap();
}

english_hello_mock.assert();

{
    // Place another request to GET /hello
    let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
    stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    stream.flush().unwrap();
}

french_hello_mock.assert();

If you’re expecting more than 1 request, you can use the Mock::expect method to specify the exact amount of requests:

§Example

use std::net::TcpStream;
use std::io::{Read, Write};

let mut s = mockito::Server::new();

let mock = s.mock("GET", "/hello").expect(3).create();

for _ in 0..3 {
    // Place a request
    let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
    stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    stream.flush().unwrap();
}

mock.assert();

You can also work with ranges, by using the Mock::expect_at_least and Mock::expect_at_most methods:

§Example

use std::net::TcpStream;
use std::io::{Read, Write};

let mut s = mockito::Server::new();

let mock = s.mock("GET", "/hello").expect_at_least(2).expect_at_most(4).create();

for _ in 0..3 {
    // Place a request
    let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
    stream.write_all("GET /hello HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    stream.flush().unwrap();
}

mock.assert();

The errors produced by the Mock::assert method contain information about the tested mock, but also about the last unmatched request, which can be very useful to track down an error in your implementation or a missing or incomplete mock. A colored diff is also displayed:

colored-diff.png

Color output is enabled by default, but can be toggled with the color feature flag.

Here’s an example of how a Mock::assert error looks like:

> Expected 1 request(s) to:

POST /users?number=one
bob

...but received 0

> The last unmatched request was:

POST /users?number=two
content-length: 5
alice

> Difference:

# A colored diff

You can also use the Mock::matched method to return a boolean for whether the mock was called the correct number of times without panicking

§Example

use std::net::TcpStream;
use std::io::{Read, Write};

let mut s = mockito::Server::new();

let mock = s.mock("GET", "/").create();

{
    let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
    stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    stream.flush().unwrap();
}

assert!(mock.matched());

{
    let mut stream = TcpStream::connect(s.host_with_port()).unwrap();
    stream.write_all("GET / HTTP/1.1\r\n\r\n".as_bytes()).unwrap();
    let mut response = String::new();
    stream.read_to_string(&mut response).unwrap();
    stream.flush().unwrap();
}
assert!(!mock.matched());

§Non-matching calls

Any calls to the Mockito server that are not matched will return 501 Not Implemented.

Note that mocks are matched in reverse order - the most recent one wins.

§Cleaning up

As mentioned earlier, mocks are cleaned up whenever the server goes out of scope. If you need to remove them earlier, you can call Server::reset to remove all mocks registered so far:

let mut s = mockito::Server::new();

s.mock("GET", "/1").create();
s.mock("GET", "/2").create();
s.mock("GET", "/3").create();

s.reset();

// Nothing is mocked at this point

…or you can call Mock::remove to remove a single mock:

let mut s = mockito::Server::new();

let m1 = s.mock("GET", "/1").create();
let m2 = s.mock("GET", "/2").create();

m1.remove();

// Only m2 is available at this point

§Debug

Mockito uses the env_logger crate under the hood to provide useful debugging information.

If you’d like to activate the debug output, introduce the env_logger crate to your project and initialize it before each test that needs debugging:

#[test]
fn example_test() {
    let _ = env_logger::try_init();
    // ...
}

Run your tests with:

RUST_LOG=mockito=debug cargo test

Structs§

  • Contains information about an error occurence
  • Stores information about a mocked request. Should be initialized via Server::mock().
  • Stores a HTTP request
  • One instance of the mock server.
  • A handle around a pooled Server object which dereferences to Server.
  • Options to configure a mock server. Provides a default implementation.

Enums§

  • The type of an error
  • Allows matching the request path, headers or body in multiple ways: by the exact value, by any value (as long as it is present), by regular expression or by checking that a particular header is missing.

Traits§