2023-03-21 00:40:43 +00:00
|
|
|
+++
|
|
|
|
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:
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` sh
|
2023-03-21 00:40:43 +00:00
|
|
|
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`:
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` rust
|
2023-03-21 00:40:43 +00:00
|
|
|
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:
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` rust
|
2023-03-21 00:40:43 +00:00
|
|
|
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:
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` rust
|
2023-03-21 00:40:43 +00:00
|
|
|
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()
|
|
|
|
}
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
```
|
2023-03-21 00:40:43 +00:00
|
|
|
|
|
|
|
And finally, in a file named `src/main.rs`, we instantiate the server:
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` rust
|
2023-03-21 00:40:43 +00:00
|
|
|
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:
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` toml
|
2023-03-21 00:40:43 +00:00
|
|
|
[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:
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` sh
|
2023-03-21 00:40:43 +00:00
|
|
|
$ cargo run
|
|
|
|
```
|
|
|
|
|
|
|
|
And in another, type and see the replies:
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` sh
|
2023-03-21 00:40:43 +00:00
|
|
|
$ 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.
|
|
|
|
|
Pre-commit checks and test refactorings.
Re-reading the text, I made a number of changes. The first is that, while it is
nice that Rust allows us to have unit tests in the file whose functionality
we're testing, it's also nice to have the tests somewhere separate, and to have
the tests be a little more modular.
In the `./tests` folder, you can now see the same `health_check` test as the
original, but in an isolated and cleaned-up form. Most importantly, the server
startup code is now in its own function, with a correct return type that
includes a handle to the spawned thread and the address on which that server is
listening; tests can be run in parallel on many different ports and a lot of
code duplication is eliminated.
``` rust
type NullHandle = JoinHandle<()>;
async fn spawn_server() -> (SocketAddr, NullHandle) {
let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap()).unwrap();
let addr = listener.local_addr().unwrap();
let handle: NullHandle = tokio::spawn(async move {
axum::Server::from_tcp(listener)
.unwrap()
.serve(app().into_make_service())
.await
.unwrap();
});
(addr, handle)
}
```
It is also possible now to add new tests in a straightforward manner. The
Hyper API is not that much different from the Actix request API, and the Axum
extractors seem to be straightforward. I suspect that what I'm looking at here
with the handle is the idea that, when it goes out of scope, it calls a d
In the introduction I said I was going to be neglecting CI/CD, since I'm a solo
developer. That's true, but I do like my guardrails. I like not being able to
commit garbage to the repository. So I'm going to add some checks, using
[Pre-Commit](https://pre-commit.com/).
Pre-Commit is a Python program, so we'll start by installing it. I'm using a
local Python environment kickstarted with
[Pyenv](https://github.com/pyenv/pyenv).
``` sh
$ pip install pre-commit
```
And inside your project, in the project root, you hook it up with the following commands:
``` sh
$ pre-commit install
$ pre-commit sample-config > .pre-commit-config.yaml
```
I'm going with the default from the rust pre-commit collection, so my
`.pre-commit-config.yaml` file looks like this:
``` yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.1.0
hooks:
- id: check-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: check-yaml
- id: end-of-file-fixer
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/pre-commit/pre-commit
rev: v2.5.1
hooks:
- id: validate_manifest
- repo: https://github.com/doublify/pre-commit-rust
rev: master
hooks:
- id: fmt
- id: cargo-check
- id: clippy
```
... and with that, every time I try to commit my code, it will not let me until
these tests pass. And I *like* that level of discipline. This is low-level
validation; it won't catch if I put addition where I meant subtraction, or if I
have a comparison going in the wrong direction, but at least the basics are
handled and, more importantly, the formatting and styling is consistent
throughout all of my code.
2023-03-22 00:52:44 +00:00
|
|
|
``` rust
|
2023-03-21 00:40:43 +00:00
|
|
|
#[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.
|