How to properly do integration tests in a private API?

Hello everyone,

I'm currently building a private API used as a backend for my application. I added a few features in it and everything is working fine. Now I want to do some testing because I tend to break old features when I'm adding a new one and it will save me some time. Since it's mostly a CRUD, I don't have a lot of unit testing to do. However, I do have a lot of integration testing to create.

What I've done so far was to modify my app from binary to libary, set up a "tests" folder to do my integration tests and create an employee_test.rs. In this employee_test, I try to test the creation of an employee.

type TestResult = Result<(), Box<dyn std::error::Error>>;

#[actix_rt::test]
async fn test_add_get_employee() -> TestResult {
    let pool = common::config::setup_test_db().await;

    let first_name = "TestFirst";
    let last_name = "TestLast";
    let title = "Test Engineer";

    // Use the public constructor/factory method from Employee
    let new_employee = Employee::create_test_employee(first_name, last_name, title);

    // --- Test Execution ---
    // Add the employee and get their ID
    let employee_id = add_new_employee(&pool, new_employee.clone()).await?; 
    let retrieved_employee = get_employee_info(&pool, employee_id).await?;

    assert_eq!(retrieved_employee.id, Some(employee_id));

    sqlx::query("DELETE FROM employees WHERE id = $1")
        .bind(employee_id)
        .execute(&pool)
        .await?;

    Ok(())
}

Concerning my employee model :

#[cfg_attr(test, derive(Debug, PartialEq))]
#[derive(Serialize,Deserialize,FromRow,Clone, Default)]
#[sqlx(default)]
pub struct Employee {
    pub id : Option<i32>,
    first_name: String, 
    last_name: String,
    title: String,
    ...
}

impl Employee {
    pub fn create_test_employee(first_name: &str, last_name: &str, title: &str) -> Self {
        Employee {
            id: None,
            first_name: first_name.to_string(),
            last_name: last_name.to_string(),
            title: title.to_string(),
            ...
        }
    }
}

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, title
        )
        VALUES (
            $1, $2, $3
        )
        RETURNING id
        "#
    )
        .bind(employee.first_name)
        .bind(employee.last_name)
        .bind(employee.title)
        .fetch_one(pool)
        .await;

    match id {
        Ok(id) => Ok(id),
        Err(e) => Err(EmployeeError::DatabaseError(e)),
    }
}

As you can see, I only assert the id because the other fields can't be asserted.

My big issue is the property readability, should I set every field as public ? I've read that it won't be an issue if it's a private API but still, I'm kinda in a mixed feeling doing so. Is a getter/setter my solution ? Because it will make the file so much bigger and those getter setter will only be used for tests.

Do you have a better implementation idea ? Maybe my solution isn't really suitable for long term.

Packages used : actix-web, sqlx, serde

I'd personally write getters, but if you prefer public fields, I don't see why you shouldn't make them public (maybe adding a #[non_exhaustive] to the type).

Because I'm afraid that setting every field as public will cause some security issues. But maybe there aren't any real security concerns in this context, and I'm just overthinking the problem :grin:

By the way, if I set everything public, can't I just create my employee directly in the test ? Isn't it better to juste leave my Employee file to do only the necessary of the logic?

If you're thinking along the lines of exposing sensitive user data, making a field private won't help.

Not if you tag it with #[non_exhaustive], then you won't be able to construct instances with Employee { ... } syntax.

I was thinking of validation but I think in the end, setting every field as public isn't a big deal (I'll validate most fields when needed and I prefer to create objects directly in the tests so getter/setter will be kinda "redundant").