In brief, this commit: * Updates to the latest upstream 'cookie', fixing a memory leak. * Make changes to 'CookieJar' observable only through 'pending()'. * Deprecates 'Client::new()' in favor of 'Client::tracked()'. * Makes 'dispatch()' on tracked 'Client's synchronize on cookies. * Makes 'Client::untracked()' actually untracked. This commit updates to the latest 'cookie' which removes support for 'Sync' cookie jars. Instead of relying on 'cookie', this commit implements an op-log based 'CookieJar' which internally keeps track of changes. The API is such that changes are only observable through specialized '_pending()' methods.
8.7 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:
-
Construct a
Rocket
instance that represents the application.let rocket = rocket::ignite(); # let _ = rocket;
-
Construct a
Client
using theRocket
instance.# use rocket::local::blocking::Client; # let rocket = rocket::ignite(); let client = Client::tracked(rocket).unwrap(); # let _ = client;
-
Construct requests using the
Client
instance.# use rocket::local::blocking::Client; # let rocket = rocket::ignite(); # let client = Client::tracked(rocket).unwrap(); let req = client.get("/"); # let _ = req;
-
Dispatch the request to retrieve the response.
# use rocket::local::blocking::Client; # let rocket = rocket::ignite(); # 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 used
transparently as a Response
value. 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.
The Response
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.body_string
: returns the body data as aString
.body_bytes
: returns the body data as aVec<u8>
.
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;
# #[get("/")]
# fn hello() -> Response<'static> {
# Response::build()
# .header(ContentType::Plain)
# .header(Header::new("X-Special", ""))
# .sized_body("Expected Body".len(), Cursor::new("Expected Body"))
# .finalize()
# }
use rocket::local::blocking::Client;
use rocket::http::{ContentType, Status};
let rocket = rocket::ignite().mount("/", routes![hello]);
let client = Client::tracked(rocket).expect("valid rocket instance");
let mut response = client.get("/").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(), Some("Expected Body".into()));
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::Rocket {
rocket::ignite().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.
# fn rocket() -> rocket::Rocket { rocket::ignite() }
# 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:
# fn rocket() -> rocket::Rocket { rocket::ignite() }
# use rocket::local::blocking::Client;
# let client = Client::tracked(rocket()).expect("valid rocket instance");
let mut response = client.get("/").dispatch();
Finally, we ensure that the response contains the information we expect it to. Here, we want to ensure two things:
- The status is
200 OK
. - 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::ignite().mount("/", routes![hello]);
# let client = Client::tracked(rocket).expect("valid rocket instance");
# let mut response = client.get("/").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;
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
fn rocket() -> rocket::Rocket {
rocket::ignite().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 mut response = client.get("/").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some("Hello, world!".into()));
}
}
# 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_world/src/main.rs:7:1
|
7 | #[get("/")]
| ^^^^^^^^^^^
|
= note:
fn rocket_route_fn_hello<'_b>(
__req: &'_b ::rocket::Request,
__data: ::rocket::Data
) -> ::rocket::handler::Outcome<'_b> {
let responder = hello();
::rocket::handler::Outcome::from(__req, responder)
}
This corresponds to the facade request handler Rocket has generated for the
hello
route.