Basic CRUD with rust using tide - tests improvements
Tags: rust, tide, sqlx
Published: 2021-07-14 14:41:24

Hi all again, in the last [post](https://javierviola.com/post/03-basic-crud-with-rust-using-tide-move-to-db/) 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] (https://collectednotes.com/javier/03-basic-crud-with-rust-using-tide-move-to-db#ci-bonus)) and tie those loose ends in the ci.

### Improving tests

First, one thing to improve in our tests is start using [surf](https://docs.rs/surf/2.1.0/surf/) as client since is the client recommended by [http-rs](https://github.com/http-rs).

So, let's add `surf` as dev-dependency

```toml
[dev-dependencies]
surf = "2.1.0"
```

And then in our code, let's use surf as client en each test

```rust
(...)
    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...

```bash
❯ 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

```rust
let d: Dino = res.body_json().await?;
assert_json_eq!(dino, d);
```

Let's run the tests again ones we add the `json asserts`...

```bash
❯ 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.

```rust
#[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.

```rust
    #[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

```yml
    - 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](https://github.com/pepoviola/tide-basic-crud/pull/4) with this test improvements and check if all the steps works as expected

![image](https://user-images.githubusercontent.com/363911/99393158-afe5cb00-28bb-11eb-98ff-bc295ef9d2cd.png)

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.

```rust
        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 )

```rust
        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](https://github.com/http-rs/tide/releases/tag/v0.15.0) 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

```toml
[dependencies]
tide = "0.15.0"
async-std = { version = "1.7.0", features = ["attributes"] }
```

And in our code the main function now looks like this

```rust
#[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!