Rocket Tutorial 03 part II: Proper testing.
Tags: rust, api, beginners, tutorial
Published: 2020-11-22 17:45:58

# Rocket Tutorial 03 part II: Proper testing.

[<span>Photo by <a href="https://unsplash.com/@kylejglenn?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Kyle Glenn</a> on <a href="https://unsplash.com/s/photos/access-denied?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span>, modified (cropped)]

We will continue to build a CRUD API with Rocket.

This is the part II of the tutorial 3. It is in between tutorial 3 and tutorial 4. The reason is... testing.
We have to do bl**dy testing, because... a programmer ought to do what a programmer ought to do, right?

So, just skip this tutorial and pass to tutorial 4 if you don't feel like it, with the promise you will at least clone the repo and have the tests done.

This time I wrote for you a lot of code, but generally I will gloss over it, and chit chat on the choices made. In fact, I think it more profitable to you that I explain the reasons why, instead of going over every little detail of the code.

Indeed, the code for this tutorial can be found in this repository: [github.com/davidedelpapa/rocket-tut](https://github.com/davidedelpapa/rocket-tut), and has been tagged for your convenience:

```bash
git clone https://github.com/davidedelpapa/rocket-tut.git
cd rocket-tut
git checkout tags/tut3pII
```

## Introduction

I understand that testing is on of the most boring activities that we as programmers could do. Thus, if you are not interested to know just now how to test the API you are free to skip this.

However, I do recommend to check it out later, because ensuring proper testing of each route must be a duty in a real world project.

Last time we tested things out with `curl` (and `jq`). This time we will use Rust proper tools

Let's update our *tests/basic_test.rs*:

```rust
use lazy_static;
use rocket::http::{ContentType, Status};
use rocket_tut::data::db::ResponseUser;
```

We will make use of the `ResponseUser` we created in order to parse also the API response for the tests.

`ping_test()` has not changed, it didn't have to. Instead, let's see how to update `user_list_rt_test()`:

```rust
#[test]
fn user_list_rt_test(){
    let client = common::setup();
    let mut response = client.get("/api/users").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    let mut response_body = response.body_string().unwrap();
    response_body.retain(|c| !c.is_numeric());
    assert_eq!(response_body, "[]");
}
```

We eliminated the useless scaffold API response:

```rust
assert_eq!(response.body_string(), Some("{\"status\":\"Success\",\"message\":\"List of users\"}".into()));
```

In its place we added:

```rust
let mut response_body = response.body_string().unwrap();
```

That gets the response's body for analysis.
Then, `retain()` parses each `char` in a `String` retaining only those specified in the closure in the predicate.
In this case whatever thing is not numeric (`!c.is_numeric()`).
What is left is a `"[]"`.

## Independent tests

You may ask why did not we check if the response in the above is simply equal to `"[0]"`. 

In reality we do not know if it will be 0 or any other number: in fact, each test is run in a different thread, and by the time the thread that tests `user_list_rt_test()` is executed, another test that inserts a user might have been already executed, and we have now already a user!
 
Indeed, we have to make **each test totally independent**. Any other solution is less than adequate. In fact one may think to test all endpoints in one test; but this renders difficult to know which endpoints fails after a change. In other words, we could not make sure that there has not been a regression, not easily at least. 

Moreover, we could devise a way to make the tests execute on only one thread, and sequentially. However, testing the multi-thread response is a plus, because we might discover more easily the presence of any race condition. Besides, this ways tests are done faster. Sadly, you might discover very soon how long testing takes (wait for the end of this tutorial!).

In order to keep each test independent, we will be inserting a new user for each of the test of the endpoints, and test specifically against that user's info.

Moreover, we need a new *dev-dependency* to check the response more easily: we will use [serde_json](https://docs.serde.rs/serde_json/):

```bash
cargo add serde_json --dev
```

Now we can import it at the beginning of *tests/basic_test.rs*:

```rust
use serde_json;
```

And now we will use it inside the next route:

```rust
#[test]
fn new_user_rt_test(){
    let client = common::setup();
    let mut response = client.post("/api/users")
        .header(ContentType::JSON)
        .body(r##"{
            "name": "John Doe",
            "email": "j.doe@m.com",
            "password": "123456"
        }"##)
        .dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    let response_body = response.body_string().expect("Response Body");
    let user: ResponseUser = serde_json::from_str(&response_body.as_str()).expect("Valid User Response");
    assert_eq!(user.name, "John Doe");
    assert_eq!(user.email, "j.doe@m.com");
}
```

We send the info in the body in a rough way: of course we could construct a `InsertableUser`, then pass it to the body with `json!` for example. But using a string literal is acceptable and quick.
Instead, we parse the answer with `serde_json` to a `ResponseUser`, and we test the `name` and `email` of the newly created user.

## Insert a new user, check only on that user

For the next route, as we have said, we need to first insert a user, then we check for its info:

```rust
#[test]
fn info_user_rt_test(){
    let client = common::setup();
    let mut response_new_user = client.post("/api/users")
        .header(ContentType::JSON)
        .body(r##"{
            "name": "Jane Doe",
            "email": "jane.doe@m.com",
            "password": "123456"
        }"##)
        .dispatch();
    let response_body = response_new_user.body_string().expect("Response Body");
    let user_new: ResponseUser = serde_json::from_str(&response_body.as_str()).expect("Valid User Response");
    let id = user_new.id;
    let mut response = client.get(format!("/api/users/{}", id)).dispatch();
    let response_body = response.body_string().expect("Response Body");
    let user: ResponseUser = serde_json::from_str(&response_body.as_str()).expect("Valid User Response");
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    assert_eq!(user.name, "Jane Doe");
    assert_eq!(user.email, "jane.doe@m.com");
    assert_eq!(user.id, id);
}
```

As you can see: first we insert a new user, then we extract its `id` from the response, in order to insert it in the next request.

At this point we can get info on the user, and check against its `id`, `name` and `email`.

Similarly, in the following routes: We insert a new user, extract its id, test if the route works. 

Of course, in order to delete the user we need to provide its password.

The same thing applies when we want to change the password. Actually there we need to provide also a new password.

You get the knack of it, go and check out the code, and if something's unclear, post a comment, and we'll discuss it together.

## Test for fails

You might think that this is all there is to test. Well, no, actually it is not: checking that a route returns the expected result is not sufficient. 

Checking the *correctness* of the response is only half of the basic testing to be done. We should check also that the API returns errors any time it has to. Besides, even when returning errors, we must make sure that no undefined behavior happens, and that a user that wants to interface with our API knows how to detect and error. Imagine that our API returns a 400 (Success) code when instead an error happened...

For that reason I created another suite of tests, a "negative" one, which tests out for failures. You can find it inside *tests/failures_test.rs*.

I'm not going to comment any of its code here. 
Besides, I wrote some comments that I hope will be useful to understand the code directly.

The only things of notice are:

1. The suite needs to provide explicitly failing cases and use sometimes `assert_ne!` macro; 
2. there are many fails we can cause to happen, but apart from checking an explicitly nonexistent route as a `generic_fail`, we kept on trying to make the API fail the most common ways it can fail: missing or *malforming* something in the requests, in order to trigger explicitly the `ApiResponse::err()` we implemented in *src/routes/user.rs*
3. we make the second ranked GET fail as well, but we made sure that even in failing the API would call the right route (the second one). Check the code to know how we did it.

We can now run `cargo test` and proudly see all tests passing, if everything went smooth.

## Conclusions

It has been a lot of testing, but I hope you understand that this is a very useful thing to do, albeit sometimes it is considered "boring."

Next time we will introduce a database to secure our data.

Stay tuned!