Hello everyone,
I'm working on integration tests in my private API configuration. For this, I'm using actix_web for everything routing related and sqlx for database queries. I removed some things from the code I share (for privacy) so it's probably breaking but you get the idea.
My employee_test.rs look like this :
mod common;
use common::config::SITE_ID;
use chrono::NaiveDate;
use sqlx::PgPool;
use pretty_assertions::{assert_eq, assert_ne};
use backend::query::employees::{Employee, get_employee_info, add_new_employee, update_employee, get_all_employees};
pub fn create_test_employee(site_id: i32) -> Employee {
Employee {
id: None,
first_name: "Test".to_string(),
last_name: "Employee".to_string(),
}
}
async fn cleanup_test_data(pool: &PgPool) -> Result<(), sqlx::Error> {
sqlx::query("DELETE FROM employees").execute(pool).await?;
Ok(())
}
/// Test cases for each function in employee. This test the basic of the function.
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[actix_rt::test]
async fn test_add_and_get_one_employee() -> TestResult {
let pool = common::config::setup_test_db().await;
let new_employee = create_test_employee(SITE_ID);
let employee_id = add_new_employee(&pool, new_employee.clone()).await?;
// Retrieve the employee we just added
let retrieved_employee = get_employee_info(&pool, employee_id).await?;
assert_eq!(retrieved_employee.id, Some(employee_id));
assert_eq!(retrieved_employee.first_name, new_employee.first_name);
assert_eq!(retrieved_employee.last_name, new_employee.last_name);
cleanup_test_data(&pool).await?;
Ok(())
}
#[actix_rt::test]
async fn test_get_all_employees() -> TestResult {
let pool = common::config::setup_test_db().await;
let employee_count = get_all_employees(&pool, SITE_ID).await?.iter().count();
// Only one employee should be in the database because he was previously created and he isn't deleted.
assert_eq!(employee_count, 0, "Expected 0 employee, got {}", employee_count);
let new_employee = create_test_employee(SITE_ID);
add_new_employee(&pool, new_employee.clone()).await?;
let new_employee_count = get_all_employees(&pool, SITE_ID).await?.iter().count();
assert_eq!(new_employee_count, 1, "Expected 1 employee, got {}", new_employee_count);
cleanup_test_data(&pool).await?;
Ok(())
}
#[actix_rt::test]
async fn test_update_an_employee() -> TestResult {
let pool = common::config::setup_test_db().await;
//Add an employee. He's by default active.
let new_employee = create_test_employee(SITE_ID);
let employee_id = add_new_employee(&pool, new_employee.clone()).await?;
// Retrieve the employee we just added
let mut retrieved_employee_to_modify = get_employee_info(&pool, employee_id).await?;
assert_eq!(retrieved_employee_to_modify.id, Some(employee_id));
assert_eq!(retrieved_employee_to_modify.first_name, new_employee.first_name);
assert_eq!(retrieved_employee_to_modify.last_name, new_employee.last_name);
//We modify only first and last name
retrieved_employee_to_modify.first_name = "Newtestfirstname".to_string();
retrieved_employee_to_modify.last_name = "Newtestlastname".to_string();
update_employee(&pool, retrieved_employee_to_modify, employee_id).await?;
let modified_employee = get_employee_info(&pool, employee_id).await?;
assert_eq!(modified_employee.first_name, "Newtestfirstname", "Excepected Newtestfirstname for first name, got {}", modified_employee.first_name);
assert_eq!(modified_employee.last_name, "Newtestlastname", "Excepected Newtestfirstname for last name, got {}", modified_employee.last_name);
cleanup_test_data(&pool).await?;
Ok(())
}
What I want to test is everything that I have in employee.rs :
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, FromRow, Error};
use chrono::{Local, NaiveDate, NaiveDateTime};
use crate::helper::option_naive_date;
#[derive(Serialize,Deserialize,FromRow,Clone, Default)]
#[sqlx(default)]
pub struct Employee {
//General information
pub id : Option<i32>,
pub first_name: String,
pub last_name: String,
}
#[derive(Debug, thiserror::Error)]
pub enum EmployeeError {
#[error("Employee with id {0} not found")]
NotFound(i32),
#[error("Database error: {0}")]
DatabaseError(#[from] Error),
}
/// # Returns
/// A list of employees if successful or a EmployeeError if something goes wrong
pub async fn get_all_employees(pool: &PgPool, site_id: i32) -> Result<Vec<Employee>, EmployeeError> {
let employees = sqlx::query_as::<_, Employee>(
r#"
SELECT
e.id as "id",
e.first_name as "first_name",
e.last_name as "last_name"
FROM
employees AS e
LEFT JOIN
sites site ON e.site_id = site.id
WHERE
e.site_id = $1
"#
)
.bind(site_id)
.fetch_all(pool)
.await;
match employees {
Ok(employees) => Ok(employees),
Err(e) => Err(EmployeeError::DatabaseError(e)),
}
}
/// # Returns
/// The employee object if successful or a EmployeeError if something goes wrong
pub async fn get_employee_info(pool: &PgPool, employee_id: i32) -> Result<Employee, EmployeeError> {
let employee = sqlx::query_as::<_, Employee>(
r#"
SELECT
e.id as "id",
e.first_name as "first_name",
e.last_name as "last_name"
FROM
employees AS e
WHERE
e.id = $1
"#
)
.bind(employee_id)
.fetch_one(pool)
.await;
match employee {
Ok(employees) => Ok(employees),
Err(Error::RowNotFound) => Err(EmployeeError::NotFound(employee_id)),
Err(e) => Err(EmployeeError::DatabaseError(e))
}
}
/// # Returns
/// Ok if successful or a EmployeeError if something goes wrong
pub async fn add_new_employee(pool: &PgPool, employee: Employee) -> Result<i32, EmployeeError> {
let id = sqlx::query_scalar(
r#"
INSERT INTO employees (
first_name, last_name
)
VALUES (
$1, $2
)
RETURNING id
"#
)
.bind(employee.first_name)
.bind(employee.last_name)
.fetch_one(pool)
.await;
match id {
Ok(id) => Ok(id),
Err(e) => Err(EmployeeError::DatabaseError(e)),
}
}
/// # Returns
/// Ok if successful or a EmployeeError if something goes wrong
pub async fn update_employee(pool: &PgPool, employee: Employee, employee_id: i32) -> Result<(), EmployeeError> {
let result = sqlx::query(
r#"
UPDATE employees
SET first_name = $1,
last_name = $2
WHERE id = $3
"#
)
.bind(employee.first_name)
.bind(employee.last_name)
.bind(employee_id)
.execute(pool)
.await;
match result {
Ok(_) => Ok(()),
Err(e) => Err(EmployeeError::DatabaseError(e)),
}
}
As you can see, since it need a database connection, it falls into integration test otherwise I may have done some unit tests with mocking (I haven't done that because it will require a lot of code refactor).
So the issue is that one of the tests fails because they all start at the same time. I fixed the issue using the command :
cargo test -- --test-threads=1
But I read that it's bad using that. How can I improve my code then ?
Thank you.