Using Stainless with Rocket
January 8, 2017
In this Blog post we will see how Rocket, Stainless and Serde work together to create easy and usable testing.
This blog posts assumes Rust nightly from at least version 1.15. Others haven’t been tested, so YMMV.
Rocket is a new Rust web framework that has been released to the public late last year. It aims to provide a simple way to write and maintain fast and safe web applications.
I have been waiting for something like this for quite some time, from the way it looks it tries to be the Rails of the Rust world.
One part of that is being able to easily test your applications. After all, without tests it is almost impossible to be confident that the output conforms to what is expected, especially in an evolving codebase.
I’ve been recently wanting to try out stainless as well. Stainless
is a plugin that allows you to write tests easier through a macro like syntax.
Especially the before_each
part of testing seemed very useful. Check out it’s
project page for some example!
Setting up
We will be using a simple rocket application that will just send back a JSON
formatted string at the /status
endpoint.
Create the rust project:
cargo new --bin rocket_stainless
Created binary (application) `rocket_stainless` project
cd rocket_stainless/
Now, let’s add rocket and stainless as dependencies:
cargo add rocket
cargo add rocket_contrib
cargo add rocket_codegen
cargo add serde
cargo add serde_derive
cargo add --dev stainless
cargo add
is part of the usefulcargo-edit
package of utilities
We also need to enable the testing feature when testing. Edit the Cargo.toml
and add to the dev-dependencies
table the following line:
rocket = { version = "0.1.4", features = ["testing"] }
Be sure to match up the version with whatever rocket version you are using!
Next step is setting everything up in src/main.rs
:
#![feature(plugin)]
#![plugin(rocket_codegen)]
#![cfg_attr(test, plugin(stainless))]
extern crate rocket;
extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
mod routes {
use rocket_contrib::JSON;
#[derive(Serialize)]
pub struct Status {
status: String,
hello: String,
}
#[get("/status")]
pub fn status() -> JSON<Status> {
JSON(Status {
status: String::from("Ok"),
hello: String::from("world"),
})
}
}
fn main() {
rocket::ignite().mount("/", routes![routes::status]).launch();
}
The code should be fairly self-explanatory. In the first part we setup the
executable to include the rocket_codegen
plugin and also include the stainless
plugin if we are compiling for tests. Then we have a small module (to keep
things clean) where we declare a struct to serialize in a Rocket endpoint. In
the main function we then launch rocket and and the server is up! (in orbit)
Okay, we are all set up. You can now run it with cargo run
and it should open
a local server:
neikos ~/p/r/b/rocket_stainless (master) [130]> cargo run
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/rocket_stainless`
🔧 Configured for development.
=> listening: localhost:8000
=> logging: Normal
🛰 Mounting '/':
=> GET /status
🚀 Rocket has launched from http://localhost:8000...
GET /status:
=> Matched: GET /status
=> Outcome: Success
=> Response succeeded.
// On another Shell
neikos ~> curl localhost:8000/status
{"status":"Ok","hello":"world"}
Now it’s time to get to the…
Testing
We now have a working Rocket application that returns a piece of JSON on
/status
. Let’s write up some tests for it.
We add this to the src/main.rs
:
#[cfg(test)]
#[allow(unused_variables)]
mod tests {
use rocket;
use rocket::testing::MockRequest;
use rocket::http::{Status, Method};
use routes;
describe! route_tests {
before_each {
let rocket = rocket::ignite().mount("/", routes![routes::status]);
}
describe! status {
before_each {
let mut req = MockRequest::new(Method::Get, "/status");
let mut res = req.dispatch_with(&rocket);
let body_str = res.body().and_then(|b| b.into_string()).unwrap();
}
it "responds with status OK 200" {
assert_eq!(res.status(), Status::Ok);
}
it "responds with OK" {
assert!(body_str.contains("Ok"));
}
it "responds with hello world" {
assert!(body_str.contains("hello"));
assert!(body_str.contains("world"));
}
}
}
}
This is a very simple test suite that just checks the above status endpoint. It should give one a simple overview of how these awesome crates work together seamlessly! There are still some errors here and there due to needing rust nightly, but all those I’ve encountered so far were during compilation and not in runtime, and usually I was doing something wrong.
We can now run the tests and check if our endpoint behaves correctly:
cargo test
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/rocket_stainless-d93b1173e0670f3f
running 3 tests
test tests::route_tests::status::responds_with_OK ... ok
test tests::route_tests::status::responds_with_hello_world ... ok
test tests::route_tests::status::responds_with_status_OK_200 ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
Looks like it does, yay!
This concludes this post; we have now a working test suite that is easy to extend without need to write a lot of boilerplate and are able to focus on the important bits of the tests.
I would love to hear back what kind of testing strategies you use! So do send me a message on reddit or shoot (launch?) me an email.