13

Wiremock: async HTTP mocking to test Rust applications

 4 years ago
source link: https://www.lpalmieri.com/posts/2020-04-13-wiremock-async-http-mocking-for-rust-applications/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

TL;DR

I released wiremock , a new crate that provides HTTP mocking to test Rust applications.

 1use wiremock::{MockServer, Mock, ResponseTemplate};
 2use wiremock::matchers::{method, path};
 3
 4#[async_std::main]
 5async fn main() {
 6    // Start a background HTTP server on a random local port
 7    let mock_server = MockServer::start().await;
 8
 9    // Arrange the behaviour of the MockServer adding a Mock:
10    // when it receives a GET request on '/hello' it will respond with a 200.
11    Mock::given(method("GET"))
12        .and(path("/hello"))
13        .respond_with(ResponseTemplate::new(200))
14        // Mounting the mock on the mock server - it's now effective!
15        .mount(&mock_server)
16        .await;
17    
18    // If we probe the MockServer using any HTTP client it behaves as expected.
19    let status = surf::get(format!("{}/hello", &mock_server.uri()))
20        .await
21        .unwrap()
22        .status();
23    assert_eq!(status.as_u16(), 200);
24
25    // If the request doesn't match any `Mock` mounted on our `MockServer` 
26    // a 404 is returned.
27    let status = surf::get(format!("{}/missing", &mock_server.uri()))
28        .await
29        .unwrap()
30        .status();
31    assert_eq!(status.as_u16(), 404);
32}
33

The name wiremock is a reference to WireMock.Net , a .NET port of the original Wiremock from Java.

You can spin up as many mock HTTP servers as you need to mock all the 3rd party APIs that your application interacts with.

Mock HTTP servers are fully isolated: tests can be run in parallel, with no interference. Each server is shut down when it goes out of scope (e.g. end of test execution).

wiremock provides out-of-the-box a set of matching strategies, but more can be defined to suit your testing needs via the Match trait.

wiremock is asynchronous: it is compatible (and tested) against both async_std and tokio as runtimes.

Why did we need another HTTP mocking crate?

ARNR3if.gif

The backstory

I spent part of last week working on a project that might lead to the first Rust REST API in production at my current company.

As it happens when you have been advocating for a technology, it was not enough to get the job done.

I set out to write the best possible sample : a showcase of what an idiomatic Rust API should look like when it comes to logging, error handling, metrics, testing, domain modelling, etc. - a code base my colleagues could use as a reference if they were to choose Rust for a future project.

BZbqQ3m.gif

All in all things went pretty smoothly - everything I needed was available as a somewhat polished crate, already provided by the Rust community.

Everything but one key piece: HTTP mocking.

The problem

It is extremely valuable to have a set of tests in your suite that interact with your service via its public API, as a user, without any knowledge of its inner workings.

These API tests can be used to verify acceptance criteria for user stories and prevent regressions.

They are as well generally easier to maintain: they are fairly decoupled from the implementation details, with a focus on the behaviour that is visible to a user of the API.

The caveat: no API is an island, especially in a microservice architecture - your service is likely to interact with the public APIs of a number of external dependencies.

When performing API tests you generally do not want to spin up those external dependencies in your continuous integration pipeline - if your microservice architecture is intricate enough, you might end spinning up tens of services to test a tiny piece of functionality in your API under test. The setup alone could take a significant amount of time - best to run those tests in a test cluster, as a final pre-deployment check.

Furthermore, you might simply not be able to spin up some of those dependencies (e.g. 3rd party SaaS services).

For our run-it-on-every-commit CI pipeline, we can get away with most of the value without putting in a crazy amount of effort: we can use HTTP mocking .

We spin up an HTTP server in the background for each of our tests, define a set of request-response scenarios ( return response A if you receive request B ) and then configure our application to use the mock server in lieu of the real service.

The available options

Two existing crates might have provided what I needed: mockito and httpmock .

Unfortunately, they both suffer by a set of limitations which I didn’t want to live with:

  • You can only run a single mock server, hence you can only mock a single external API;
  • Tests must be run sequentially;
  • No way to define custom request matchers to extend the functionality provided out of the box by the crate.

Easter, 4 days-long break - I set out to write a new HTTP mocking crate, wiremock .

MVJjauf.gif

Wiremock

wiremock provides fully-isolated MockServer s : start takes care of finding a random port available on your local machine which is assigned to the new server.

You can use one instance of MockServer for each test and for each 3rd party API you need to mock - each server is shut down when it goes out of scope (e.g. end of test execution).

 1use wiremock::{MockServer, Mock, ResponseTemplate};
 2use wiremock::matchers::method;
 3
 4#[async_std::main]
 5async fn main() {
 6    // Arrange
 7    let mock_server_one = MockServer::start().await;
 8    let mock_server_two = MockServer::start().await;
 9
10    assert!(mock_server_one.address() != mock_server_two.address());
11
12    let mock = Mock::given(method("GET")).respond_with(ResponseTemplate::new(200));
13    // Registering the mock with the first mock server - it's now effective!
14    // But it *won't* be used by the second mock server!
15    mock_server_one.register(mock).await;
16
17    // It matches our mock
18    let status = surf::get(&mock_server_one.uri())
19        .await
20        .unwrap()
21        .status();
22    assert_eq!(status.as_u16(), 200);
23
24    // This would have matched our mock, but we haven't registered it 
25    // for `mock_server_two`!
26    // Hence it returns a 404, the default response when 
27    // no mocks matched on the mock server.
28    let status = surf::get(&mock_server_two.uri())
29        .await
30        .unwrap()
31        .status();
32    assert_eq!(status.as_u16(), 404);
33}
34

wiremock provides a set of matching strategies out of the box - see the matchers module for a complete list.

You can also define your own matchers using the Match trait, as well as using Fn closures.

How does it work?

Each instance of MockServer is, under the hood, a pair of actors running on Bastion :

When a request hits a MockServer :

  • the server actor tries to parse it from a async_std::net::TcpStream using async-h1 ;
  • the parsed request is passed as a message to the mock actor;
  • the mock actor checks all mocks against it, one by one - if one matches, the associated response is returned; if nothing matches a 404 is returned;
  • the server actor responds to the caller.

The two actors are killed when MockServer is dropped.

Going forward

Well, testing for the upcoming Rust API is surely going to be shiny!

For the overall Rust community, I foresee two main development directions when it comes to wiremock :

  • Spying is the biggest piece of functionality wiremock is currently missing: being able to assert if a mock matched, or how many times it did.
    Quite useful when you want to verify that certain side-effects have been triggered by your application.
  • More matching strategies for common use cases will make their way into wiremock as time goes forward.

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK