github twitter mastodon
Integration testing a service written in Rust and Iron
27 Dec 2016
4 minutes read

I recently wrote a small (you could say micro) web service in Rust using the Iron framework. It had some unit tests but I wasn’t all too sure whether I was using Iron the right way. So I wanted to have some more tests in the project to cover these parts as well.

This post shows how to go from a simple binary to a project with integration tests, all written in Rust.

Let’s use an example of a well-known service, left-pad (note: that wasn’t the service I deployed).

This is src/main.rs:

extern crate iron;
extern crate urlencoded;

use iron::prelude::*;
use iron::status;
use urlencoded::UrlEncodedQuery;

fn main() {
    Iron::new(handler)
        .http(("0.0.0.0", 3000))
        .expect("Unable to start server");
}

fn handler(request: &mut Request) -> IronResult<Response> {
    let query = request.get_ref::<UrlEncodedQuery>().unwrap();
    if let Some(str_params) = query.get("str") {
        if let Some(len_params) = query.get("len") {
            let s = &str_params[0];
            let len = len_params[0].parse().expect("Error parsing len query param");
            let padded = leftpad(s, len);
            return Ok(Response::with((status::Ok, padded)));
        }
    }
    return Ok(Response::with((status::BadRequest, "Specify str and len query params")));
}

fn leftpad(s: &str, len: usize) -> String {
    format!("{: >width$}", s, width = len)
}

The actual logic for left-pad is in a separate function, so we can write a test for it by adding:

#[test]
fn test_leftpad() {
    assert_eq!(leftpad("foo", 2), "foo".to_string());
    assert_eq!(leftpad("foo", 4), " foo".to_string());
    assert_eq!(leftpad("foo", 6), "   foo".to_string());
    assert_eq!(leftpad("", 4), "    ".to_string());
}

But what about the rest? We could write a unit test for the handler function and try to fake the request, but that wouldn’t tell us whether we’re using the libraries correctly.

So we’d like to write an integration test instead.

The Rust book has a chapter about testing, and a section about integration tests. It tells us to add a new file like tests/integration_test.rs, so let’s try that:

extern crate iron_testing_example;

#[test]
fn it_works() {
}

But we get an error compiling it:

error[E0463]: can't find crate for `iron_testing_example`
 --> tests/integration_test.rs:1:1
  |
1 | extern crate iron_testing_example;
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ can't find crate

What’s going on? Well, the problem is that this kind of test only works with libraries, because Rust links the test code to the library and then executes it.

In our case we like that the server can be run as a binary, so we’d like to keep that.

Fortunately, there’s a nice solution. You can have a project that builds both a library and a binary using that library!

So we can move all the code into src/lib.rs, and change the main function to look like this:

pub fn start_server(host: &str, port: u16) -> iron::Listening {
    Iron::new(handler)
        .http((host, port))
        .expect("Unable to start server")
}

Then, src/main.rs just becomes:

extern crate iron_testing_example;

use iron_testing_example::start_server;

fn main() {
    start_server("0.0.0.0", 3000);
}

Now in our integration test, we can start a server and send requests to it. Let’s use the recently released reqwest crate for this:

extern crate iron_testing_example;
extern crate reqwest;

use iron_testing_example::start_server;
use reqwest::Client;
use std::io::Read;

#[test]
fn it_works() {
    let mut server = start_server("127.0.0.1", 0);
    let client = Client::new().unwrap();

    let url = format!("http://{}:{}/?str={}&len={}",
                      server.socket.ip(), server.socket.port(), "foo", 5);
    let mut response = client.get(&url).send().unwrap();
    let mut s = String::new();
    response.read_to_string(&mut s).unwrap();

    assert_eq!("  foo", s);

    server.close().unwrap();
}

If you have multiple tests like this, you can extract a few things to make the setup nicer. And we can use the Drop trait to automatically close the server at the end of a test as well:

struct TestServer(Listening);

impl TestServer {
    fn new() -> TestServer {
        TestServer(start_server("127.0.0.1", 0))
    }

    fn url(&self) -> String {
        format!("http://{}:{}", self.0.socket.ip(), self.0.socket.port())
    }
}

impl Drop for TestServer {
    fn drop(&mut self) {
        self.0.close().expect("Error closing server");
    }
}

The actual test method then contains very little setup code.

If you’re wondering whether this way of testing is too slow compared to unit tests, don’t worry, this is Rust we’re talking about. Doing a cargo test in this project only takes about 250 ms on my ageing laptop.

I put the whole project here if you want to have a look at all the code, and there’s a build set up as well (see Pipelines in sidebar):

https://bitbucket.org/robinst/iron-testing-example

Thanks for reading!


Back to posts