202 lines
6.2 KiB
Markdown
202 lines
6.2 KiB
Markdown
+++
|
|
title = "Hello World, You Need Testing"
|
|
date = 2023-03-20T17:38:12Z
|
|
weight = 2
|
|
+++
|
|
Zero-to-Production's project is writing a web service that signs people up for
|
|
an email newsletter. The first task in the book is to set up a "Hello World!"
|
|
application server.
|
|
|
|
The book uses the [Actix-web](https://actix.rs/) web framework, but I've chosen
|
|
to implement it using [Axum](https://github.com/tokio-rs/axum) server, the
|
|
default server provided by the [Tokio](https://github.com/tokio-rs/tokio)
|
|
asynchronous runtime.
|
|
|
|
Although the book is only two years old, it is already out-of-date with respect
|
|
to some commands. `cargo add` is now provided by default. The following
|
|
commands installed the tools I'll be using:
|
|
|
|
``` sh
|
|
cargo add --features tokio/full --features hyper/full tokio hyper \
|
|
axum tracing tracing-subscriber
|
|
```
|
|
|
|
- axum: The web server framework for Tokio.
|
|
- tokio: The Rust asynchronous runtime. Has single-threaded (select) and
|
|
multi-threaded variants.
|
|
- [hyper](https://hyper.rs/): An HTTPS request/response library, used for testing.
|
|
- [tracing](https://crates.io/crates/tracing): A debugging library that works
|
|
with Tokio.
|
|
|
|
We start by defining the core services. In the book, they're a greeter ("Hello,
|
|
World"), a greeter with a parameter ("Hello, {name}"), and a health check
|
|
(returns a HTTP 200 Code, but no body). Actix-web hands a generic Request and
|
|
expects a generic request, but Axum is more straightforward, providing
|
|
`IntoResponse` handlers for most of the basic Rust types, as well as some for
|
|
formats via Serde, Rust's standard serializing/deserializing library for
|
|
converting data from one format to another.
|
|
|
|
All of these go into `src/lib.rs`:
|
|
|
|
``` rust
|
|
async fn health_check() -> impl IntoResponse {
|
|
(StatusCode::OK, ())
|
|
}
|
|
|
|
async fn anon_greet() -> &'static str {
|
|
"Hello World!\n"
|
|
}
|
|
|
|
async fn greet(Path(name): Path<String>) -> impl IntoResponse {
|
|
let greeting = String::from("He's dead, ") + name.as_str();
|
|
let greeting = greeting + &String::from("!\n");
|
|
(StatusCode::OK, greeting)
|
|
}
|
|
```
|
|
|
|
<aside>Axum's documentation says to [avoid using `impl
|
|
IntoResponse`](https://docs.rs/axum/latest/axum/response/index.html#regarding-impl-intoresponse)
|
|
until you understand how it really works, as it can result in confusing issues
|
|
when chaining response handlers, when a handler can return multiple types, or
|
|
when a handler can return either a type or a [`Result<T,
|
|
E>`](https://doc.rust-lang.org/std/result/), especially one with an error.</aside>
|
|
|
|
We then define the routes that our server will recognize. This is
|
|
straightforward and familiar territory:
|
|
|
|
``` rust
|
|
fn app() -> Router {
|
|
Router::new()
|
|
.route("/", get(anon_greet))
|
|
.route("/:name", get(greet))
|
|
.route("/health_check", get(health_check))
|
|
}
|
|
```
|
|
|
|
We then define a function to *run* the core server:
|
|
|
|
``` rust
|
|
pub async fn run() {
|
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
|
tracing::info!("listening on {}", addr);
|
|
axum::Server::bind(&addr)
|
|
.serve(app().into_make_service())
|
|
.await
|
|
.unwrap()
|
|
}
|
|
```
|
|
|
|
And finally, in a file named `src/main.rs`, we instantiate the server:
|
|
|
|
``` rust
|
|
use ztp::run;
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
run().await
|
|
}
|
|
```
|
|
|
|
To make this "work," we need to define what `ztp` means, and make a distinction
|
|
between the library and the CLI program.
|
|
|
|
In the project root's `Cargo.toml` file, the first three sections are needed to
|
|
define these relationships:
|
|
|
|
``` toml
|
|
[package]
|
|
name = "ztp"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
|
|
[lib]
|
|
path = "src/lib.rs"
|
|
|
|
[[bin]]
|
|
path = "src/main.rs"
|
|
name = "ztp"
|
|
```
|
|
|
|
It is the `[package.name]` feature that defines how the `use` statement in
|
|
`main.rs` will find the library. The `[[bin]]` clause defines the name of the
|
|
binary when it is generated. <aside>The double brackets around the `[[bin]]`
|
|
clauses is there to emphasize to the TOML parser that there can be more than one
|
|
binary. There can be only one library per package, but it is possible for a Rust
|
|
project to have more than one package, called "crates," per project. </aside>
|
|
|
|
This project should now be runnable. In one window, type:
|
|
|
|
``` sh
|
|
$ cargo run
|
|
```
|
|
|
|
And in another, type and see the replies:
|
|
|
|
``` sh
|
|
$ curl http://localhost:3000/
|
|
Hello, World!
|
|
$ curl http://localhost:3000/Jim
|
|
He's dead, Jim!
|
|
$ curl -v http://localhost:3000/health_check
|
|
> GET /health_check HTTP/1.1
|
|
> Host: localhost:3000
|
|
> User-Agent: curl/7.81.0
|
|
> Accept: */*
|
|
< HTTP/1.1 200 OK
|
|
< content-length: 0
|
|
< date: Tue, 21 Mar 2023 00:16:43 GMT
|
|
```
|
|
|
|
In the last command, the *verbose* flag shows us what we sent to the server, and
|
|
what came back. We expected a "200 OK" flag and a zero-length body, and that's
|
|
what we got.
|
|
|
|
## Testing
|
|
|
|
In order to unit-test a web server, we must spawn a copy of it in order to
|
|
exercise its functions. We'll use Tokio's `spawn` function to create a new
|
|
server, use hyper to request data from the server, and finally Rust's own native
|
|
test asserts to check that we got what we expected.
|
|
|
|
``` rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode},
|
|
};
|
|
use std::net::{SocketAddr, TcpListener};
|
|
|
|
#[tokio::test]
|
|
async fn the_real_deal() {
|
|
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>()
|
|
.unwrap()).unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
|
|
tokio::spawn(async move {
|
|
axum::Server::from_tcp(listener)
|
|
.unwrap()serve(app().into_make_service()).await.unwrap();
|
|
});
|
|
|
|
let response = hyper::Client::new()
|
|
.request(
|
|
Request::builder().uri(format!("http://{}/", addr))
|
|
.body(Body::empty()).unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
|
|
assert_eq!(&body[..], b"Hello World!\n");
|
|
}
|
|
}
|
|
```
|
|
|
|
One interesting trick to observe in this testing is the port number specified in
|
|
the `TcpListener` call. It's zero. When the port is zero, the `TcpListener` will
|
|
request from the kernel the first-free-port. Normally, you'd want to know
|
|
exactly what port to call the server on, but in this case both ends of the
|
|
communication are aware of the port to use and we want to ensure that port isn't
|
|
hard-coded and inconveniently already in-use by someone else.
|