Hi all again, in the last post we refactor the code to persist the information in a relational database, postgresql in our case.
Let's start where we left last time (ci bonus) and tie those loose ends in the ci.
Improving tests
First, one thing to improve in our tests is start using surf as client since is the client recommended by http-rs.
So, let's add surf
as dev-dependency
[dev-dependencies]
surf = "2.1.0"
And then in our code, let's use surf as client en each test
(...)
let res = surf::Client::with_http_client(app)
.get("https://example.com/dinos")
.await?;
assert_eq!(200, res.status());
(...)
let mut res = surf::Client::with_http_client(app)
.post("https://example.com/dinos")
.body(serde_json::to_string(&dino)?)
.await?;
Great, let's run the tests...
⯠cargo test
Finished test [unoptimized + debuginfo] target(s) in 0.20s
Running target/debug/deps/tide_basic_crud-1a926f88350611fd
running 5 tests
test tests::list_dinos ... ok
test tests::create_dino ... ok
test tests::delete_dino ... ok
test tests::get_dino ... ok
test tests::update_dino ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Awesome! But at this point we are only asserting the status code. Let's check now also the returned payload, for that we will use the crate assert-json-diff
that add two macros:
-
assert_json_eq
: macro used to compare two JSON values for an exact match. -
assert_json_include
: macro used to compare two JSON values for an inclusive match.
For example, add this lines to the get_dino
test
let d: Dino = res.body_json().await?;
assert_json_eq!(dino, d);
Let's run the tests again ones we add the json asserts
...
⯠cargo test
Finished test [unoptimized + debuginfo] target(s) in 1.44s
Running target/debug/deps/tide_basic_crud-1a926f88350611fd
running 5 tests
test tests::list_dinos ... ok
test tests::create_dino ... ok
test tests::delete_dino ... ok
test tests::get_dino ... ok
test tests::update_dino ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Great! now we are also validating the returned payload.
We have a couple more of TODOs
before finish the improvements. First, we need to clear the dinos
table before run each test since we always want to create an isolated test case. To accomplish that, let's create a module (mod
) in our main file for the tests and add a helper function to clear the dinos
table.
#[cfg(test)]
mod tests {
use super::*;
use lazy_static::lazy_static;
use sqlx::query;
async fn clear_dinos() -> Result<(),Box<dyn std::error::Error>> {
let db_pool = make_db_pool(&DB_URL).await;
sqlx::query("DELETE FROM dinos").execute(&db_pool).await?;
Ok(())
}
(...)
And in each test we need to run clear_dinos
before make any change/request.
#[async_std::test]
async fn create_dino() -> tide::Result<()> {
dotenv::dotenv().ok();
clear_dinos().await.expect("Failed to clear the dinos table");
(...)
Great, so one more task to go. We need to set the ci
(gh actions) to run the tests. For that we set a new block in ci.yml
to run those tests
- name: Run test
run: cargo test
env:
DATABASE_URL: postgres://postgres:postgres@localhost:${{ job.services.postgres.ports[5432] }}/tide
And we are ready to create a new PR with this test improvements and check if all the steps works as expected
Nice! we now have the ci configured.
Beyond the happy path
Until now our test only check the happy path and we are not testing errors. Let's add some basic test cases for cover those
- Create a duplicate dino with an existing key, should return 409
We need to handler the insert error and return the appropriated error since using the ?
here will bubble the error to the caller.
let row : Dino = match query_as!(
Dino,
r#"
INSERT INTO dinos (id, name, weight, diet) VALUES
($1, $2, $3, $4) returning id, name, weight, diet
"#,
dino.id,
dino.name,
dino.weight,
dino.diet
)
.fetch_one(&db_pool)
.await {
Ok( r) => r,
Err( e ) => {
// TODO: we may want to cast the error here.
let err = Error::new(409,e);
return Err(err);
}
};
(...)
- Get/Delete/Update dino with a non existing key, should return 404
In this cases we only need to send an invalid key ( e.g. a new one and should works )
let res = surf::Client::with_http_client(app)
.delete(format!("https://example.com/dinos/{}", &Uuid::new_v4()))
.await?;
assert_eq!(404, res.status());
Upgrade bonus
Also, this week a new version of tide was released with a new way to start the servers
Tide v0.15.0 introduces a new way to start servers: Server::bind. This enables separating "open the socket" from "start accepting connections" which Server::listen does for you in a single call.
Let's update our code to use this new version, first the deps in cargo
[dependencies]
tide = "0.15.0"
async-std = { version = "1.7.0", features = ["attributes"] }
And in our code the main function now looks like this
#[async_std::main]
async fn main() {
dotenv::dotenv().ok();
tide::log::start();
let db_url = std::env::var("DATABASE_URL").unwrap();
let db_pool = make_db_pool(&db_url).await;
let app = server(db_pool).await;
let mut listener = app.bind("127.0.0.1:8080").await.expect("can't bind the port");
for info in listener.info().iter() {
println!("Server listening on {}", info);
}
listener.accept().await.unwrap();
}
That's all for today, I was planned to add the implementation of tera
as render engine but will cover that in the next post
and keep this one focused in tests.
As always, I write this as a learning journal and there could be another more elegant and correct way to do it and any feedback is welcome.
Thanks!
Top comments (0)