Zero2Prod: Day 1 – Writing tests
Extract the app logic to a function
async fn main() -> std::io::Result<()> {
let _ = run().await?;
Ok(())
}
pub fn run() -> Result<Server, std::io::Error> {
let server = HttpServer::new(|| create_app())
.bind("127.0.0.1:8080")?
.run();
Ok(server)
}
fn create_app() -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = actix_web::Error,
InitError = (),
>,
> {
App::new()
.route("/health", web::get().to(health))
.route("/", web::get().to(greet))
.route("/{name}", web::get().to(greet))
}
fn create_app()
return anApp
that implementsServiceFactory
trait.- we need to provide all the type params:
Response
,Config
,Error
,InitError
.
pub trait ServiceFactory<Req> {
/// Responses given by the created services.
type Response;
/// Errors produced by the created services.
type Error;
/// Service factory configuration.
type Config;
/// The kind of `Service` created by this factory.
type Service: Service<Req, Response = Self::Response, Error = Self::Error>;
/// Errors potentially raised while building a service.
type InitError;
/// The future of the `Service` instance.g
type Future: Future<Output = Result<Self::Service, Self::InitError>>;
/// Create and return a new service asynchronously.
fn new_service(&self, cfg: Self::Config) -> Self::Future;
}
Where to put the tests?
Next to our code in an embedded test module:
#[cfg(test)]
mod tests {
use super::*;
// write tests here
}
In an external tests folder:
$ ls
src/
tests/
health.rs
As part of public documentation (doc tests):
/// Check if a number is even.
/// ```rust
/// use zero2prod::is_even;
///
/// assert!(is_even(2));
/// assert!(!is_even(1));
/// ```
pub fn is_even(x: u64) -> bool {
x % 2 == 0 }
Execute with cargo test
:
cargo test --test health
Test the /health endpoint with actix_web
// main.rs
async fn health(req: HttpRequest) -> impl Responder {
HttpResponse::Ok().json(json!({ "status": "ok" }))
}
Write the first test using actix_web::test
:
// tests.rs
#[cfg(test)]
use crate::health;
use crate::App;
use actix_web::{http::StatusCode, test, web};
#[actix_web::test] // 👈 macro
async fn test_health_ok() {
let app = test::init_service(
create_app() // 👈 the app logic
).await;
let req = test::TestRequest::get().uri("/health").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
// test the response body
let body = response.text().await.expect("Failed to read response body");
assert_eq!(body, r#"{"status":"ok"}"#);
}
- We init the app by calling
create_app()
insidetest::init_service()
. - Create a request by
test::TestRequest::get()
. - Execute the request and get the response by
test::call_service(&app, req).await
.
Blackbox test the /health endpoint
Implement spawn_app()
by using tokio::spawn()
fn spawn_app() {
let server = newsletter::run().expect("Failed to run newsletter");
// Start the server as background task
let _ = tokio::spawn(server);
}
Write the test
#[tokio::test] // 👈 macro
async fn health_test_ok() {
spawn_app();
let client = reqwest::Client::new();
let response = client
.get("http://127.0.0.1:8080/health")
.send()
.await
.expect("Failed to send request");
assert!(response.status().is_success());
// assert_eq!(response., Some(2));
}
Use random port
- Use
TcpListener::bind("127.0.0.1:0")
to listen on a random port - And return that port to be used in the tests
// Spawn the app on a random port
let listener = TcpListener::bind(addr).expect("random port");
let port = listener.local_addr().unwrap().port();
Notes
tokio::spawn
shutdown all tasks spawned when it are dropped.tokio::test
spins up new runtime at the start of the test, and clean it up when the test ends
Author
I'm Oliver Nguyen. A software maker working mostly in Go and JavaScript. I enjoy learning and seeing a better version of myself each day. Occasionally spin off new open source projects. Share knowledge and thoughts during my journey. Connect with me on , , , and .