loading...
Cover image for Rust HTTP Testing with httpmock

Rust HTTP Testing with httpmock

alexliesenfeld profile image Alexander Liesenfeld Updated on ・5 min read

HTTP mocking libraries allow you to simulate HTTP responses so you can easier test code that depends on third-party APIs. This article shows how you can use httpmock to do this in Rust.

Why HTTP-Mocking?

At some point, every developer had the need to test calls to external APIs. This is especially important in (micro)service-oriented environments where services typically require access to several external dependencies, such as APIs, authentication providers, data sources, etc. These services are not always available to you. This is where mocking/stubbing tools can fill the gaps.

HTTP mocking libraries usually provide you a simple HTTP server and tools to configure it for custom request/response scenarios. We will see examples of such scenarios in the following sections.

The App

Let's suppose we are building a Rust app that will create a Github repository on our behalf. To perform these operations, we will use the Github REST API. We will then write some automated tests to verify correct behavior by simulating HTTP responses using httpmock.

Let's start!

Let's first create a new cargo package for our app and name it github_api_client:

cargo new github_api_client --bin
Enter fullscreen mode Exit fullscreen mode

We'll also need some libraries, so let's add them to Cargo.toml. We'll use

  • isahc as our HTTP client library,
  • serde_json for easy JSON serialization and deserialization, and
  • custom_error to create custom error types easily.

Cargo.toml:

[dependencies]
isahc = { version = "0.9.8", features = ["json"] }
serde_json = "1.0"
anyhow = "1.0.34"
Enter fullscreen mode Exit fullscreen mode

The Client

Now let's write some code that will allow us to access the Github REST API. We will create a structure named GithubClient that will contain all the logic required to access the Github API.

The only method our client provides is create_repo. It takes the repository name as an argument and returns a Result containing the repository URL as a string value. To keep this example simple, we use the anyhow crate for generic error handling.

The Problem

Now that we have a functional app, we need to write some tests to make sure it doesn’t have any obvious errors.

The tricky part is to find a good target for mocking so we can test the client behavior in different scenarios. In our case, the HTTP client (such as the Request::post method in our client implementation) looks like a good place to start.

Unfortunately, mocking HTTP clients is very tedious. This is because in a larger application we would need to reimplement a big chunk of the HTTP clients API to be able to simulate request/response scenarios. So what to do?

The Solution

To test our Github API client conveniently, we can use an HTTP mocking library. Such libraries can help us verify that HTTP requests sent by our client are correct and allow us to simulate HTTP responses from the Github API.

At the time of writing there are 4 noteworthy Rust libraries that can help us with this:

  • mockito
  • httpmock
  • httptest
  • wiremock.

The following comparison matrix shows how the libraries compare to each other:

Library Execution Custom Matchers Mock- able APIs Sync API Async API Stand-alone Mode
mockito serial no 1 yes no no
httpmock parallel yes yes yes yes
httptest parallel yes yes no no
wiremock parallel yes no yes no

According to the comparison matrix the most complete package is currently provided by httpmock. For this reason, we will use this one for the rest of this article (and also because I am the developer 😜).

Creating Mocks

In this section, we'll write some tests to verify our Github API client works as expected. Let’s first add the httpmock crate to our Cargo.toml:

[dev-dependencies]
httpmock = "0.5"
Enter fullscreen mode Exit fullscreen mode

Now we’re all set. Let’s create a test that will make sure “the good path” in our client implementation works as expected:

To easier grasp what this test does, we arranged it following the AAA (Arrange-Act-Assert) pattern (look at the comments).

Arrange

The first step in our test function is to create a MockServer instance (line 10). Next, we create a Mock object on the MockServer with all our request and response requirements (lines 11–18).

Notice how we used the when variable to define HTTP request requirements (lines 12-15). We used the then variable to define what data the corresponding HTTP response will contain (lines 16–17).

The mock server will only respond as we specified if it receives an HTTP request that meets all the request criteria from the when part. Otherwise, it will respond with an error message and HTTP status code 404.

Important: Observe how we set the base URL in our client to point to the mock server instead of the real Github API (line 19).

Act

Next, we trigger the method that is under test (line 22) which is the method create_repo from our GithubClient structure.

Assert

At last, we use the assert method provided by the mock object (line 25). This method makes sure that the mock server received exactly one HTTP request that matched all the mock requirements. If not, it will fail the test with a detailed problem description (see next section).

Verification

Mock objects provide an assert method which ensures our app did actually send a request to the mock server and that it matched the mock spec (the when part). If not, this method will fail the test. In this case httpmock will try to find a request in its request journal that is most similar to the mock spec. It will then identify the differences between the two so you can easily spot unexpected values.

To demonstrate this feature, we will modify our client to send text/plain in the content-type header. If we rerun the test, we'll see that this change is detected and the test fails now with the following message:

At least one request has been received, but none exactly matched the mock specification.
Here is a comparison with the most similar request (request number 1): 
1 : Expected header with name 'Content-Type' and value 'application/json' to be present in the request but it wasn't.
------------------------------------------------------------------------------------------
Expected:                [key=equals, value=equals]   Content-Type=application/json
Actual (closest match):                               content-type=text/plain
Enter fullscreen mode Exit fullscreen mode

Depending on the IDE you are using, you will also be able to see the differences between the expected and actual value in a differences viewer. For example, this is how it would look like in IntelliJ or CLion:

1 idKzYnNLhfzGjgdEE7rIgQ

Conclusion

This article showed how httpmock can be used to test HTTP-based API clients in Rust. On one hand, it allowed us to verify that our app is sending HTTP requests which contain all the required information. On the other hand, we could simulate HTTP responses to see if our app behaves as expected.

You can find the source code from this article on Github.

GitHub logo alexliesenfeld / blog-rust-http-testing-with-httpmock

Code for my blog post https://dev.to/alexliesenfeld/rust-http-testing-with-httpmock-2mi0

Discussion

pic
Editor guide
Collapse
5422m4n profile image
Sven Assmann

Great article.
I wonder if there are mock libraries that would record the responses on demand and can turn into a mode that those recorded responses are replayed?

On a different node js project we have used such a thing and it was super handy to not handwrite responses or store them manually and replay them by hand.

PS: connect-prism is the name of the JavaScript lib

Collapse
alexliesenfeld profile image
Alexander Liesenfeld Author

Thank you. I am not aware of any Rust libraries that do this at the moment but it would be an interesting idea for a new feature in httpmock :-)

Collapse
5422m4n profile image
Sven Assmann

Actually I’m also not aware of any rust lib that can do it. I think it would be a pretty unique feature. It saves so much time and can help to have one test case act as end to end test and turn it into a unit / integration test without the need of changing the test code, if for instance a configuration flag could change the modus.
Do you feel inspired to include it in your httpmock Library? 😁