Diesel: Documentation: How to do unit testing

Created on 8 Feb 2018  路  8Comments  路  Source: diesel-rs/diesel

What are you trying to accomplish?

I'm building a web server using Diesel. I want to unit test my code that access the DB via diesel. But I'm finding it unclear how to do this.

What is the expected output?

A documentation guide that explain best practices for unit testing while using Diesel and how to.

For example if I choose to hit the DB, how to clean it before each test, or run tests in transactions. Maybe something about factories.

If I would like to mock db call, how to achieve this.

Most helpful comment

I am doing 2 things:

  1. I am using an empty test database which I set using an environment variable. This allows me to test things like counting how many records are in the database without the results being affected by previously existing data.

    #[cfg(test)]
    fn get_database_url() -> String {
        dotenv::dotenv().ok();
        std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable is not set.")
    }
    

    I run the tests like this:

    $ DATABASE_URL=postgres://user:pass@localhost/my-test-db cargo test
    
  2. I am using the test_transaction() method described above. This starts a transaction which is rolled back at the end of the test, so no data is ever written into my test database. This is working fine, except that if you have any expected error during the test the transactions ends at that point and you cannot continue with your test.

    #[test]
    fn test_create_user() {
        let connection = establish_connection(&get_database_url()).unwrap();
        let email = "[email protected]";
        let password = "mypass";
        connection.test_transaction::<_, Error, _>(|| {
            // create_user() is the function under test, it creates a user in the database and
            // returns it.
            let user = create_user(&connection, email, password).unwrap();
    
            // Check that the returned user object has the correct values.
            assert_eq!(user.email, email);
    
            // Creating a second user with the same email address should result in an error.
            let second_user = create(&connection, email, "some_other_password").unwrap_err();
            match second_user {
                Err(DatabaseError(UniqueViolation, _)) => {},
                _ => panic!()
            }
    
            // Unfortunately at this point a database error has occurred and the transaction has
            // been closed so we cannot do any more database interactions. I have not yet found a
            // way around this. I simply open a new test transaction at this point.
    
            Ok(())
        });
    }
    

    And here is establish_connection() which is just outputting some errors in the CLI for my convenience. This helps to figure out if there is a problem connecting to the database when running tests in CI infrastructure or a docker setup:

    pub fn establish_connection(database_url: &str) -> Result<PgConnection, ConnectionError> {
        match PgConnection::establish(&database_url) {
            Ok(value) => Ok(value),
            Err(e) => {
                error!("Could not connect to PostgreSQL.");
                error!("Error connecting to {}", database_url);
                Err(e)
            }
        }
    }
    

Of course this is not unit testing but functional testing. I am also interested in how to do real unit testing with a mock database connection, but I have not yet found out how to do this.

Here are some real life examples in my project: https://github.com/pfrenssen/firetrack/tree/master/db - (search for test_transaction()). Please note this is my first Rust project so this might not be following best practices, but at least I have working tests for all my database interactions which can serve as inspiration.

All 8 comments

Hi - Can I work on this documentation issue and submit PR?

Sure.

@rajcspsg did you end up creating anything for this?

@joelgallant Try looking into diesels own test suit for examples how to write unit tests.
It basically boils down to have a function to create connection somewhere and calling begin_test_transaction on that connection before returning it to the test case.

Was there any work on this done? Or can someone point me to good examples?
I've written a microservice in Rust and am running into this issue. Any help is appreciated!

I am doing 2 things:

  1. I am using an empty test database which I set using an environment variable. This allows me to test things like counting how many records are in the database without the results being affected by previously existing data.

    #[cfg(test)]
    fn get_database_url() -> String {
        dotenv::dotenv().ok();
        std::env::var("DATABASE_URL").expect("DATABASE_URL environment variable is not set.")
    }
    

    I run the tests like this:

    $ DATABASE_URL=postgres://user:pass@localhost/my-test-db cargo test
    
  2. I am using the test_transaction() method described above. This starts a transaction which is rolled back at the end of the test, so no data is ever written into my test database. This is working fine, except that if you have any expected error during the test the transactions ends at that point and you cannot continue with your test.

    #[test]
    fn test_create_user() {
        let connection = establish_connection(&get_database_url()).unwrap();
        let email = "[email protected]";
        let password = "mypass";
        connection.test_transaction::<_, Error, _>(|| {
            // create_user() is the function under test, it creates a user in the database and
            // returns it.
            let user = create_user(&connection, email, password).unwrap();
    
            // Check that the returned user object has the correct values.
            assert_eq!(user.email, email);
    
            // Creating a second user with the same email address should result in an error.
            let second_user = create(&connection, email, "some_other_password").unwrap_err();
            match second_user {
                Err(DatabaseError(UniqueViolation, _)) => {},
                _ => panic!()
            }
    
            // Unfortunately at this point a database error has occurred and the transaction has
            // been closed so we cannot do any more database interactions. I have not yet found a
            // way around this. I simply open a new test transaction at this point.
    
            Ok(())
        });
    }
    

    And here is establish_connection() which is just outputting some errors in the CLI for my convenience. This helps to figure out if there is a problem connecting to the database when running tests in CI infrastructure or a docker setup:

    pub fn establish_connection(database_url: &str) -> Result<PgConnection, ConnectionError> {
        match PgConnection::establish(&database_url) {
            Ok(value) => Ok(value),
            Err(e) => {
                error!("Could not connect to PostgreSQL.");
                error!("Error connecting to {}", database_url);
                Err(e)
            }
        }
    }
    

Of course this is not unit testing but functional testing. I am also interested in how to do real unit testing with a mock database connection, but I have not yet found out how to do this.

Here are some real life examples in my project: https://github.com/pfrenssen/firetrack/tree/master/db - (search for test_transaction()). Please note this is my first Rust project so this might not be following best practices, but at least I have working tests for all my database interactions which can serve as inspiration.

@pfrenssen Thanks for your quick and extensive reply. I'll be looking into it! I just wish there would be an official best practise or sth like it

@Mastermindaxe There are two reasons why there exists no official best practice or guide on this topic:

  • There is no one fits all use cases solution here. Depending on your actual requirements it may be a good idea to simply create a new database on each test run, use test_transaction or something completely different.
  • Someone needs to write such a guide. This is quite a lot of work, where at least I do not have the bandwidth for currently. So feel free to submit your suggestion as PR to the draft directory here
Was this page helpful?
0 / 5 - 0 ratings