testingrustrust-cargorust-diesel

Why are my tests failing in my rust code?


I using diesel to access a Postgres DB and when i was going to write tests for my code(that is working btw) the tests failed, debugging further I've noticed that the connection with the database wasn't really working correctly during tests, but with cargo run it worked just fine, does anybody know why?

use diesel::prelude::*;
use dotenvy::dotenv;
use models::{NewTodo, ToDo};
use std::{env::{self}, io::stdin};
pub mod models;
pub mod schema;

pub fn establish_connection() -> PgConnection {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    PgConnection::establish(&database_url)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

pub fn load_menu() -> isize {
    println!("\n[1] - Create Task\n[2] - Mark Task\n[3] - Delete Task\n[4] - Delete All");
    let menu_response = get_number_input();
    match menu_response {
        1 => {
            create_task(NewTodo { title : create_title().as_str(), done : ask_done(), });
        },
        2 => {
            println!("Task Id: ");
            update_status(get_number_input());
        },
        3 => {
            println!("Task Id: ");
            delete_task(get_number_input());
        }
        4 => {
            delete_all();
        }
        _ => {
            println!("No answer available");
            return -1
        }
    }
    0
}

pub fn create_task(task : NewTodo){
    use crate::schema::to_do;
    let connection= &mut establish_connection();
    diesel::insert_into(to_do::table)
        .values(&task)
        .returning(ToDo::as_returning())
        .get_result(connection)
        .expect("Error saving new task");
}

pub fn read_tasks() -> Vec<ToDo> {
    use crate::schema::to_do::dsl::*;
    let connection= &mut establish_connection();
    let tasks = to_do.select(ToDo::as_select()).load(connection).expect("Error while loading tasks");
    tasks
}

pub fn update_status(position_list : usize) {
    use crate::schema::to_do::dsl::{to_do, done};
    let connection = &mut establish_connection();

    let tasks = read_tasks();
    let task = match tasks[position_list].done {
        false => diesel::update(to_do.find(tasks[position_list].id)).set(done.eq(true)).returning(ToDo::as_returning()).get_result(connection).unwrap(),
        true => diesel::update(to_do.find(tasks[position_list].id)).set(done.eq(false)).returning(ToDo::as_returning()).get_result(connection).unwrap(),
    };
    println!("Completed Task: {}", task.title);
}

pub fn delete_task(position_list : usize) {
    use crate::schema::to_do::dsl::*;
    let connection = &mut establish_connection();

    let tasks = read_tasks();

    let num_deleted = diesel::delete(to_do.filter(id.eq(tasks[position_list].id)))
        .execute(connection)
        .expect("Error deleting the task");

    println!("Deleted Tasks: {}", num_deleted);
}

pub fn delete_all() {
    use crate::schema::to_do::dsl::*;
    let connection = &mut establish_connection();
    let num_deleted = diesel::delete(to_do)
        .execute(connection)
        .expect("Error deleting all tasks");
    println!("Deleted {} tasks", num_deleted);
}

pub fn print_tasks() {
    let tasks = read_tasks();
    match tasks.len() {
        0 => println!("No tasks available!"),
        _ =>{  println!("You have {} tasks:", tasks.len());
                for i in 0..tasks.len() {
                    if tasks[i].done { println!("({}){} - Done", i, tasks[i].title) } else { println!("({}){} - []", i, tasks[i].title) };
                }
        }
    }
}

pub fn get_number_input() -> usize {
    let mut input = String::new();
    stdin().read_line(&mut input).expect("Failed to read line");
    input.trim().parse().expect("Expected a number") 
}

pub fn create_title() -> String {
    let mut title = String::new();
    println!("What's the name of your task?");
    stdin().read_line(&mut title).unwrap();
    title.trim().to_string()
}

pub fn ask_done() -> bool {
    println!("\nOk! Is the task done? [Y/N]",);
    let mut done = String::new();
    stdin().read_line(&mut done).unwrap();
    if done.trim().to_uppercase().as_str() == "Y" {true} else {false}
}


#[cfg(test)]
pub mod tests {
    use crate::*;
    
    #[test]
    fn test_establish_connection_success() {
        dotenvy::from_filename(".env.test").ok(); // Load the test environment
        let result = std::panic::catch_unwind(|| {
            establish_connection() // Try to establish a connection
        });

        assert!(result.is_ok(), "Connection to the database failed");
    }

    

    #[test]
    fn test_create_task() {
        delete_all();
        let task = NewTodo { title: "Test Task", done: false };
        create_task(task);

        let tasks = read_tasks();
        assert_eq!(tasks[tasks.len()-1].title, "Test Task");
    }

    #[test]
    fn test_read_tasks() {
        delete_all();
        create_task(NewTodo { title: "Task 1", done: false });
        create_task(NewTodo { title: "Task 2", done: true });

        let tasks = read_tasks();
        assert_eq!(tasks.len(), 2);
        assert_eq!(tasks[0].title, "Task 1");
        assert!(tasks[1].done);
    }

    #[test]
    fn test_update_status() {
        delete_all();
        create_task(NewTodo { title: "Task to Update", done: false });

        update_status(0);
        let updated_tasks = read_tasks();
        assert!(updated_tasks[0].done);

        update_status(0);
        let toggled_tasks = read_tasks();
        assert!(!toggled_tasks[0].done);
    }

    #[test]
    fn test_delete_task() {
        delete_all();
        create_task(NewTodo { title: "Task to Delete", done: false });
        delete_task(0);
        let remaining_tasks = read_tasks();
        assert!(remaining_tasks.is_empty());
    }
}

Don't mind the fact that I'm deleting everything on my DB, I'll fix that later!

TEST RESULTS: failures:

---- tests::test_create_task stdout ---- Deleted 0 tasks thread 'tests::test_create_task' panicked at src/lib.rs:149:9: assertion left == right failed left: "Task to Delete" right: "Test Task" note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

---- tests::test_read_tasks stdout ---- Deleted 0 tasks thread 'tests::test_read_tasks' panicked at src/lib.rs:159:9: assertion left == right failed left: 5 right: 2

---- tests::test_update_status stdout ---- Deleted 4 tasks Completed Task: Task to Update thread 'tests::test_update_status' panicked at src/lib.rs:171:9: assertion failed: updated_tasks[0].done

---- tests::test_delete_task stdout ---- Deleted 0 tasks Deleted Tasks: 1 thread 'tests::test_delete_task' panicked at src/lib.rs:184:9: assertion failed: remaining_tasks.is_empty()

I tried delete_all() on many places of the test functions, tried using read_tasks() again, tried using the connection again to try and reset it, tried ChatGPT for ideas nothing came on sight.


Solution

  • It sounds like you expect the tests to run serially, in the order the functions appear in your code. cargo test runs tests in parallel on multiple threads, so this explains the behavior you're seeing. For example, in test_read_tasks(), after the call to delete_all(), concurrently-running tests manage to create 3 additional tasks this test isn't expecting to see, which causes the assertion that there are only 2 tasks to fail.

    There is, as far as I know, also no defined order in which tests are run even if you were to use --test-threads=1 to run them serially. My understanding is that they are run in approximately alphabetical order, but I don't believe this is actually mandated anywhere.

    If your tests depend on side effects from other tests to run successfully, then they aren't actually individual tests. The easiest way to address this would be to combine them into a single test function that runs everything in the exact order you need.

    If you add more tests later, be aware that they can run in parallel (without --test-threads=1) and in any order, and so they shouldn't touch the same state (e.g. the same database table) otherwise the side effects of one test could be visible in another.