Practical Rust Web Development - Testing
Tags: rust, diesel, actixweb, test
Published: 2019-06-04 09:31:58

---
title: Practical Rust Web Development - Testing
published: true
description: Test an actix web app
tags: rust, diesel, actixweb, test
series: Practical Rust Web Development
---

Tests are one of the most important parts of the application and mystore will not be an exception. Actix web provides a few tools we can use to perform unit and integration tests.

I'll be covering integration tests, because in my opinion are more useful, they goes from input to output with a simulation of user behavior. We should avoid mocking as much as we can, because in this way we can be certain our code will work in a similar environment.

We'll start with a new file to store our database configuration, we probably will use it for other things.

`tests/common/db_connection.rs`:

```rust
use diesel::pg::PgConnection;
use diesel::r2d2::{ Pool, ConnectionManager, PoolError };

pub type PgPool = Pool<ConnectionManager<PgConnection>>;

fn init_pool(database_url: &str) -> Result<PgPool, PoolError> {
    let manager = ConnectionManager::<PgConnection>::new(database_url);
    Pool::builder().build(manager)
}

pub fn establish_connection() -> PgPool {
    init_pool(dotenv!("DATABASE_URL_TEST")).expect("Failed to create pool")
}
```

`tests/common/mod.rs`:

```rust
pub mod db_connection;
```

We need a database just for testing, so in .env file add a new variable.

`src/.env`:

```bash
DATABASE_URL_TEST=postgres://postgres:@localhost/mystore_test
```

Let's create our test database:

```bash
$ sudo su - postgres
$ createdb mystore_test
```

Run the migrations:

```bash
DATABASE_URL=postgres://postgres:@localhost/mystore_test  diesel migration run
```
In order to run our integration tests we will need a new file, `lib.rs`, this is because Rust treats integration tests as a separate crate and we need a way to share the code we created in tests. 

Edit the `Cargo.toml` file with a new section indicating the name of the library we're going to be using and a dependency section for our tests:

`src/Cargo.toml`:

```toml
[lib]
name = "mystore_lib"
path = "src/lib.rs"

[dev-dependencies]
bytes = "0.4"
actix-http-test = "0.2.0"
```

`src/lib.rs`:

```rust
#[macro_use]
extern crate diesel;
extern crate dotenv;
extern crate serde;
extern crate serde_json;
#[macro_use] 
extern crate serde_derive;

extern crate actix;
extern crate actix_web;
extern crate bcrypt;
extern crate jsonwebtoken as jwt;
extern crate csrf_token;

#[macro_use]
extern crate dotenv_codegen;

#[macro_use] extern crate log;
extern crate env_logger;

extern crate actix_http;

pub mod schema;
pub mod db_connection;
pub mod models;
pub mod handlers;
pub mod errors;
pub mod utils;
```

I moved some lines from `main.rs` to `lib.rs`, now we can use the same code in integration tests. Now we can test our product endpoint, let's create a new file called `product_test.rs`.

`tests/product_test.rs`:

```rust
#[macro_use]
extern crate dotenv_codegen;

mod common;

mod test{
    use actix_http::HttpService;
    use actix_http_test::TestServer;
    use actix_web::{http, App, web};

    use std::str;
    use crate::common::db_connection::establish_connection;

    #[test]
    fn test() {
        let mut srv = TestServer::new(|| 
            HttpService::new(
                App::new()
                    .data(establish_connection())
                    .service(
                        web::resource("/products")
                            .route(web::get().to(::mystore_lib::handlers::products::index))
                    )
            )
        );

        let request = srv.get("/products");
        let mut response = srv.block_on(request.send()).unwrap();
        assert!(response.status().is_success());

        assert_eq!(
            response.headers().get(http::header::CONTENT_TYPE).unwrap(),
            "application/json"
        );

        let bytes = srv.block_on(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        assert_eq!(body, "[]");
    }
}
```

Let's use `TestServer` to set up a simple server for our tests, we then can assert our response was successful and gives us the expected result.

If we run the tests with `cargo test` they will fail because we still need a way to authenticate an user.

Let's add a couple of functions, one for creating our user and the other to login.

`tests/product_test.rs`:

```rust
    fn create_user() {
        use diesel::RunQueryDsl;
        use ::mystore_lib::schema::users;
        use ::mystore_lib::models::user::{ NewUser, User };
        use chrono::Local;

        let connection = establish_connection();
        let pg_pool = connection.get().unwrap();

        diesel::delete(users::table).execute(&pg_pool).unwrap();

        diesel::insert_into(users::table)
            .values(NewUser {
                email: "jhon@doe.com".to_string(),
                company: "My own personal enterprise".to_string(),
                password: User::hash_password("12345678".to_string()).unwrap(),
                created_at: Local::now().naive_local()
            })
            .get_result::<User>(&pg_pool).unwrap();
    }

    fn login(mut srv: RefMut<TestServerRuntime>) -> (HeaderValue, Cookie) {
        let request = srv
                          .post("/auth")
                          .header(header::CONTENT_TYPE, "application/json")
                          .timeout(std_duration::from_secs(600));
        let response =
            srv
                .block_on(request.send_body(r#"{"email":"jhon@doe.com","password":"12345678"}"#))
                .unwrap();
        let csrf_token = response.headers().get("x-csrf-token").unwrap();
        let cookies = response.cookies().unwrap();
        let cookie = cookies[0].clone().into_owned().value().to_string();

        let request_cookie = Cookie::build("mystorejwt", cookie)
                                         .domain("localhost")
                                         .path("/")
                                         .max_age(Duration::days(1).num_seconds())
                                         .secure(false)
                                         .http_only(false)
                                         .finish();
        (csrf_token.clone(), request_cookie.clone())
    }
```

We perform a post request against `auth` with a json content type, we add a longer timeout to tell the test to wait for the response, then we pass the body, the authenticated user, finally we get the csrf_token and the cookie from the response to use it in the rest of the tests.

You probably noted that `TestServerRuntime` is wrapped with a type called `RefMut`, that is because Rust does not allow mutation of a variable more than once, and we're going to mutate the server variable for every request, we need to do that for the headers, cookies and attachment of the body in posts and patches, just take a look for the definition for [headers](https://docs.rs/awc/0.2.0/src/awc/request.rs.html#181-196), can you see the `mut self` in the function parameter?, that's our sign to know the server will be mutating. 

Let's see an example of a forbidden mutation:

```rust
struct Books {
    name: String,
    author: String,
    subject: String,
    book_id: i32
}

fn main() {
    let mut book1 = Books { 
        name: "Rust Programming".to_string(),
        author: "Jhon Doe".to_string(),
        subject: "Programming".to_string(),
        book_id: 12
    };
    
    let name = change_name(&mut book1);
    change_author(&mut book1);
    
    println!("{}", name);
}

fn change_name(book: &mut Books) -> &String {
    book.name = "Great Rust Programming".to_string();
    &book.name
}

fn change_author(book: &mut Books) {
    book.author = "Fulano".to_string();
}
``` 

If you try to compile the previous example you will get:

```bash
error[E0499]: cannot borrow `book1` as mutable more than once at a time
  --> src/main.rs:17:19
   |
16 |     let name = change_name(&mut book1);
   |                            ---------- first mutable borrow occurs here
17 |     change_author(&mut book1);
   |                   ^^^^^^^^^^ second mutable borrow occurs here
18 |     
19 |     println!("{}", name);
   |                    ---- first borrow later used here
```


Let's add the rest of the functions we'll be needing to test the products endpoints.

```rust

    fn clear_products() {
        use diesel::RunQueryDsl;
        use ::mystore_lib::schema::products;

        let connection = establish_connection();
        let pg_pool = connection.get().unwrap();
        diesel::delete(products::table).execute(&pg_pool).unwrap();
    }

    fn create_a_product(mut srv: RefMut<TestServerRuntime>,
                            csrf_token: HeaderValue,
                            request_cookie: Cookie,
                            product: &NewProduct) -> Product {

        let request = srv
                          .post("/products")
                          .header(header::CONTENT_TYPE, "application/json")
                          .header("x-csrf-token", csrf_token.to_str().unwrap())
                          .cookie(request_cookie)
                          .timeout(std_duration::from_secs(600));

        let mut response =
            srv
                .block_on(request.send_body(json!(product).to_string()))
                .unwrap();

        assert!(response.status().is_success());

        let bytes = srv.block_on(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        serde_json::from_str(body).unwrap()
    }

    fn show_a_product(mut srv: RefMut<TestServerRuntime>,
                          csrf_token: HeaderValue,
                          request_cookie: Cookie,
                          id: &i32,
                          expected_product: &Product) {

        let request = srv
                        .get(format!("/products/{}", id))
                        .header("x-csrf-token", csrf_token.to_str().unwrap())
                        .cookie(request_cookie);

        let mut response = srv.block_on(request.send()).unwrap();
        assert!(response.status().is_success());

        assert_eq!(
            response.headers().get(http::header::CONTENT_TYPE).unwrap(),
            "application/json"
        );

        let bytes = srv.block_on(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        let response_product: Product = serde_json::from_str(body).unwrap();
        assert_eq!(&response_product, expected_product);
    }

    fn update_a_product(mut srv: RefMut<TestServerRuntime>,
                          csrf_token: HeaderValue,
                          request_cookie: Cookie,
                          id: &i32,
                          changes_to_product: &NewProduct) {

        let request = srv
                        .request(http::Method::PATCH, srv.url(&format!("/products/{}", id)))
                        .header(header::CONTENT_TYPE, "application/json")
                        .header("x-csrf-token", csrf_token.to_str().unwrap())
                        .cookie(request_cookie)
                        .timeout(std_duration::from_secs(600));

        let response =
            srv
                .block_on(request.send_body(json!(changes_to_product).to_string()))
                .unwrap();
        assert!(response.status().is_success());
    }

    fn destroy_a_product(mut srv: RefMut<TestServerRuntime>,
                          csrf_token: HeaderValue,
                          request_cookie: Cookie,
                          id: &i32) {
        let request = srv
                        .request(http::Method::DELETE, srv.url(&format!("/products/{}", id)))
                        .header(header::CONTENT_TYPE, "application/json")
                        .header("x-csrf-token", csrf_token.to_str().unwrap())
                        .cookie(request_cookie)
                        .timeout(std_duration::from_secs(600));

        let response =
            srv
                .block_on(request.send())
                .unwrap();
        assert!(response.status().is_success());
    }

    fn products_index(mut srv: RefMut<TestServerRuntime>,
                          csrf_token: HeaderValue,
                          request_cookie: Cookie,
                      mut data_to_compare: Vec<NewProduct>) {

        let request = srv
                        .get("/products")
                        .header("x-csrf-token", csrf_token.to_str().unwrap())
                        .cookie(request_cookie);

        let mut response = srv.block_on(request.send()).unwrap();
        assert!(response.status().is_success());

        assert_eq!(
            response.headers().get(http::header::CONTENT_TYPE).unwrap(),
            "application/json"
        );

        let bytes = srv.block_on(response.body()).unwrap();
        let body = str::from_utf8(&bytes).unwrap();
        let mut response_products: Vec<Product> = serde_json::from_str(body).unwrap();
        data_to_compare.sort_by_key(|product| product.name.clone());
        response_products.sort_by_key(|product| product.name.clone());

        // Can you see we're doing something weird here, 
        // we're comparing two different types, expected
        // to be the same, Vec<NewProduct> == Vec<Product>
        assert_eq!(data_to_compare, response_products);
    }
```

In the index function we expect that a `Vec<Product>` to be equal to a `Vec<NewProduct>`, how can we expect two vectors of different types to be the same?. We can use a trait for that, `PartialEq`, let's see the code in `models/product.rs`.

`src/models/product.rs`:

```rust
impl PartialEq<Product> for NewProduct {
    fn eq(&self, other: &Product) -> bool {
        let new_product = self.clone();
        let product = other.clone();
        new_product.name == Some(product.name) &&
        new_product.stock == Some(product.stock) &&
        new_product.price == product.price
    }
}
``` 

Now we're going to finish our main function:

```rust
    fn test() {

        create_user();

        let csrf_token_header =
            header::HeaderName::from_lowercase(b"x-csrf-token").unwrap();

        // RefCell is the type that allows us multiple mutations
        let srv = RefCell::new(TestServer::new(move || 
            HttpService::new(
                App::new()
                    .wrap(
                        IdentityService::new(
                            CookieIdentityPolicy::new(dotenv!("SECRET_KEY").as_bytes())
                                .domain("localhost")
                                .name("mystorejwt")
                                .path("/")
                                .max_age(Duration::days(1).num_seconds())
                                .secure(false)
                        )
                    )
                    .wrap(
                        cors::Cors::new()
                            .allowed_origin("localhost")
                            .allowed_methods(vec!["GET", "POST", "PUT", "PATCH", "DELETE"])
                            .allowed_headers(vec![header::AUTHORIZATION,
                                                  header::CONTENT_TYPE,
                                                  header::ACCEPT,
                                                  csrf_token_header.clone()])
                            .expose_headers(vec![csrf_token_header.clone()])
                            .max_age(3600)
                    )
                    .data(
                        CsrfTokenGenerator::new(
                            dotenv!("CSRF_TOKEN_KEY").as_bytes().to_vec(),
                            Duration::hours(1)
                        )
                    )
                    .data(establish_connection())
                    .service(
                        web::resource("/products")
                            .route(web::get()
                                .to(::mystore_lib::handlers::products::index))
                            .route(web::post()
                                .to(::mystore_lib::handlers::products::create))
                    )
                    .service(
                        web::resource("/products/{id}")
                            .route(web::get()
                                .to(::mystore_lib::handlers::products::show))
                            .route(web::delete()
                                .to(::mystore_lib::handlers::products::destroy))
                            .route(web::patch()
                                .to(::mystore_lib::handlers::products::update))
                    )
                    .service(
                        web::resource("/auth")
                            .route(web::post()
                                .to(::mystore_lib::handlers::authentication::login))
                            .route(web::delete()
                                .to(::mystore_lib::handlers::authentication::logout))
                    )

            )
        ));

        let (csrf_token, request_cookie) = login(srv.borrow_mut());
        clear_products();

        let shoe = NewProduct {
            name: Some("Shoe".to_string()),
            stock: Some(10.4),
            price: Some(1892)
        };

        let hat = NewProduct {
            name: Some("Hat".to_string()),
            stock: Some(15.0),
            price: Some(2045)
        };

        let pants = NewProduct {
            name: Some("Pants".to_string()),
            stock: Some(25.0),
            price: Some(3025)
        };
        let shoe_db = create_a_product(srv.borrow_mut(),
                                       csrf_token.clone(),
                                       request_cookie.clone(),
                                       &shoe);
        let hat_db = create_a_product(srv.borrow_mut(),
                                      csrf_token.clone(),
                                      request_cookie.clone(),
                                      &hat);
        let pants_db = create_a_product(srv.borrow_mut(),
                                        csrf_token.clone(), 
                                        request_cookie.clone(), 
                                        &pants);
        show_a_product(srv.borrow_mut(), 
                       csrf_token.clone(), 
                       request_cookie.clone(), 
                       &shoe_db.id, 
                       &shoe_db);
        let updated_hat = NewProduct {
            name: Some("Hat".to_string()),
            stock: Some(30.0),
            price: Some(3025)
        };
        update_a_product(srv.borrow_mut(), 
                         csrf_token.clone(), 
                         request_cookie.clone(), 
                         &hat_db.id, 
                         &updated_hat);
        destroy_a_product(srv.borrow_mut(), 
                          csrf_token.clone(), 
                          request_cookie.clone(), 
                          &pants_db.id);
        products_index(srv.borrow_mut(), 
                       csrf_token, 
                       request_cookie, 
                       vec![shoe, updated_hat]);
    }
```

## Conclusion

I just test the best case scenario, it's up to you to go and write the failure tests to see how our software behaves before an error, you can take a look at the full source code [here](https://github.com/werner/practical-rust-web/tree/v3).