Rocket/site/guide/8-testing.md
2021-08-19 19:49:23 -07:00

10 KiB

Testing

Every application should be well tested and understandable. Rocket provides the tools to perform unit and integration tests. It also provides a means to inspect code generated by Rocket.

Local Dispatching

Rocket applications are tested by dispatching requests to a local instance of Rocket. The local module contains all of the structures necessary to do so. In particular, it contains a Client structure that is used to create LocalRequest structures that can be dispatched against a given Rocket instance. Usage is straightforward:

  1. Construct a Rocket instance that represents the application.

    let rocket = rocket::build();
    # let _ = rocket;
    
  2. Construct a Client using the Rocket instance.

    # use rocket::local::blocking::Client;
    # let rocket = rocket::build();
    let client = Client::tracked(rocket).unwrap();
    # let _ = client;
    
  3. Construct requests using the Client instance.

    # use rocket::local::blocking::Client;
    # let rocket = rocket::build();
    # let client = Client::tracked(rocket).unwrap();
    let req = client.get("/");
    # let _ = req;
    
  4. Dispatch the request to retrieve the response.

    # use rocket::local::blocking::Client;
    # let rocket = rocket::build();
    # let client = Client::tracked(rocket).unwrap();
    # let req = client.get("/");
    let response = req.dispatch();
    # let _ = response;
    

Validating Responses

A dispatch of a LocalRequest returns a LocalResponse which can be inspected for validity. During testing, the response is usually validated against expected properties. These includes things like the response HTTP status, the inclusion of headers, and expected body data.

LocalResponse type provides methods to ease this sort of validation. We list a few below:

  • status: returns the HTTP status in the response.
  • content_type: returns the Content-Type header in the response.
  • headers: returns a map of all of the headers in the response.
  • into_string: reads the body data into a String.
  • into_bytes: reads the body data into a Vec<u8>.
  • into_json: deserializes the body data on-the-fly as JSON.
  • into_msgpack: deserializes the body data on-the-fly as MessagePack.

These methods are typically used in combination with the assert_eq! or assert! macros as follows:

# #[macro_use] extern crate rocket;

# use std::io::Cursor;
# use rocket::Response;
# use rocket::http::Header;
#
# #[derive(Responder)]
# #[response(content_type = "text")]
# struct Custom {
#     body: &'static str,
#     header: Header<'static>,
# }
#
# #[get("/")]
# fn hello() -> Custom {
#     Custom {
#         body: "Expected Body",
#         header: Header::new("X-Special", ""),
#     }
# }

# use rocket::local::blocking::Client;
use rocket::http::{ContentType, Status};

# let rocket = rocket::build().mount("/", routes![hello]);
# let client = Client::debug(rocket).expect("valid rocket instance");
let mut response = client.get(uri!(hello)).dispatch();

assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Plain));
assert!(response.headers().get_one("X-Special").is_some());
assert_eq!(response.into_string().unwrap(), "Expected Body");

Testing "Hello, world!"

To solidify an intuition for how Rocket applications are tested, we walk through how to test the "Hello, world!" application below:

# #[macro_use] extern crate rocket;

#[get("/")]
fn hello() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![hello])
}

Notice that we've separated the creation of the Rocket instance from the launch of the instance. As you'll soon see, this makes testing our application easier, less verbose, and less error-prone.

Setting Up

First, we'll create a test module with the proper imports:

#[cfg(test)]
mod test {
    use super::rocket;
    use rocket::local::blocking::Client;
    use rocket::http::Status;

    #[test]
    fn hello_world() {
        /* .. */
    }
}

You can also move the body of the test module into its own file, say tests.rs, and then import the module into the main file using:

#[cfg(test)] mod tests;

Testing

To test our "Hello, world!" application, we create a Client for our Rocket instance. It's okay to use methods like expect and unwrap during testing: we want our tests to panic when something goes wrong.

# #[rocket::launch]
# fn rocket() -> _ {
#     rocket::build().configure(rocket::Config::debug_default())
# }
# use rocket::local::blocking::Client;

let client = Client::tracked(rocket()).expect("valid rocket instance");

Then, we create a new GET / request and dispatch it, getting back our application's response:

# use rocket::uri;
# #[rocket::launch]
# fn rocket() -> _ {
#     rocket::build().configure(rocket::Config::debug_default())
# }

# #[rocket::get("/")]
# fn hello() -> &'static str { "Hello, world!" }

# use rocket::local::blocking::Client;
# let client = Client::tracked(rocket()).expect("valid rocket instance");
let mut response = client.get(uri!(hello)).dispatch();

Finally, we ensure that the response contains the information we expect it to. Here, we want to ensure two things:

  1. The status is 200 OK.
  2. The body is the string "Hello, world!".

We do this by checking the Response object directly:

# #[macro_use] extern crate rocket;

# #[get("/")]
# fn hello() -> &'static str { "Hello, world!" }

# use rocket::local::blocking::Client;
use rocket::http::{ContentType, Status};
#
# let rocket = rocket::build().mount("/", routes![hello]);
# let client = Client::debug(rocket).expect("valid rocket instance");
# let mut response = client.get(uri!(hello)).dispatch();

assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some("Hello, world!".into()));

That's it! Altogether, this looks like:

# #[macro_use] extern crate rocket;
# use rocket::{Rocket, Build};

#[get("/")]
fn hello() -> &'static str {
    "Hello, world!"
}


# /*
#[launch]
# */
fn rocket() -> Rocket<Build> {
    rocket::build().mount("/", routes![hello])
}

# /*
#[cfg(test)]
# */
mod test {
    use super::rocket;
    use rocket::local::blocking::Client;
    use rocket::http::Status;

    # /*
    #[test]
    # */ pub
    fn hello_world() {
        # /*
        let client = Client::tracked(rocket()).expect("valid rocket instance");
        # */
        # let client = Client::debug(rocket()).expect("valid rocket instance");
        let mut response = client.get(uri!(super::hello)).dispatch();
        assert_eq!(response.status(), Status::Ok);
        assert_eq!(response.into_string().unwrap(), "Hello, world!");
    }
}

# fn main() { test::hello_world(); }

The tests can be run with cargo test. You can find the full source code to this example on GitHub.

Asynchronous Testing

You may have noticed the use of a "blocking" API in these examples, even though Rocket is an async web framework. In most situations, the blocking testing API is easier to use and should be preferred. However, when concurrent execution of two or more requests is required for the server to make progress, you will need the more flexible asynchronous API; the blocking API is not capable of dispatching multiple requests simultaneously. While synthetic, the async_required testing example uses an async barrier to demonstrate such a case. For more information, see the rocket::local and rocket::local::asynchronous documentation.

Codegen Debug

It can be useful to inspect the code that Rocket's code generation is emitting, especially when you get a strange type error. To have Rocket log the code that it is emitting to the console, set the ROCKET_CODEGEN_DEBUG environment variable when compiling:

ROCKET_CODEGEN_DEBUG=1 cargo build

During compilation, you should see output like:

note: emitting Rocket code generation debug output
  --> examples/hello/src/main.rs:14:1
   |
14 | #[get("/world")]
   | ^^^^^^^^^^^^^^^^
   |
   = note:
    impl world {
        fn into_info(self) -> rocket::StaticRouteInfo {
            fn monomorphized_function<'_b>(
                __req: &'_b rocket::request::Request<'_>,
                __data: rocket::data::Data,
            ) -> ::rocket::route::BoxFuture<'_b> {
                ::std::boxed::Box::pin(async move {
                    let ___responder = world();
                    ::rocket::handler::Outcome::from(__req, ___responder)
                })
            }

            ::rocket::StaticRouteInfo {
                name: "world",
                method: ::rocket::http::Method::Get,
                path: "/world",
                handler: monomorphized_function,
                format: ::std::option::Option::None,
                rank: ::std::option::Option::None,
                sentinels: sentinels![&'static str],
            }
        }
    }

This corresponds to the facade request handler Rocket has generated for the hello route.