db-pool is a thread-safe database pool for running database-tied integration tests in parallel with:

  • Easy setup
  • Proper isolation
  • Automatic creation, reuse, and cleanup
  • Async support

Description

Rather than simply providing a database connection pool that allows multiple connections to the same database, db-pool maintains a pool of separate isolated databases in order to allow running database-tied tests in parallel. It also handles the lifecycles of those databases: whenever you pick a database out of the pool, you can be sure that the database is clean and ready to be used, and that no other tests are connected to the database you are using in any one test.

Motivation

When running tests against a database-tied service, such as a web server, a test database is generally used. However, this comes with its own set of difficulties:

  1. The database has to be either (a) dropped and re-created or (b) cleaned before every test.
  2. Tests have to run serially in order to avoid cross-contamination.

This leads to several issues when running tests serially:

  • Test setup and teardown is now required.
  • Dropping and creating a database from scratch can be expensive.
  • Cleaning a database instead of dropping and re-creating one requires careful execution of dialect-specific statements.

When switching to parallel execution of tests, even more difficulties arise:

  • Creating and dropping a database for each test can be expensive.
  • Sharing temporary databases across tests requires:
    • isolating databases in concurrent use
    • cleaning each database before reuse by a subsequent test
    • restricting user privileges to prevent schema modification by rogue tests
    • dropping temporary databases before or after a test run to reduce clutter

db-pool takes care of all of these concerns while supporting multiple database types, backends, and connection pools.

Databases

  • MySQL (MariaDB)
  • PostgreSQL

Backends & Pools

Sync

BackendPoolFeature
diesel/mysqlr2d2diesel-mysql
diesel/postgresr2d2diesel-postgres
mysqlr2d2mysql
postgresr2d2postgres

Async

BackendPoolFeatures
diesel-async/mysqlbb8diesel-async-mysql, diesel-async-bb8
diesel-async/mysqlmobcdiesel-async-mysql, diesel-async-mobc
diesel-async/postgresbb8diesel-async-postgres, diesel-async-bb8
diesel-async/postgresmobcdiesel-async-postgres, diesel-async-mobc
sea-orm/sqlx-mysqlsqlxsea-orm-mysql
sea-orm/sqlx-postgressqlxsea-orm-postgres
sqlx/mysqlsqlxsqlx-mysql
sqlx/postgressqlxsqlx-postgres
tokio-postgresbb8tokio-postgres, tokio-postgres-bb8
tokio-postgresmobctokio-postgres, tokio-postgres-mobc

We will use the Diesel Postgres backend in this tutorial.

The database pool has to live for the duration of the test run. We use a OnceLock to store a "lazy static".

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code)]

    // import OnceLock
    use std::sync::OnceLock;

    fn get_connection_pool() {
        // add "lazy static"
        static POOL: OnceLock<()> = OnceLock::new();
    }
}

We then need to create a privileged database configuration loaded from environment variables.

We create a .env file.

export POSTGRES_USERNAME=postgres
export POSTGRES_PASSWORD=postgres
export POSTGRES_HOST=localhost
export POSTGRES_PORT=3306

The environment variables used are optional.

Environment VariableDefault
POSTGRES_USERNAMEpostgres
POSTGRES_PASSWORD{blank}
POSTGRES_HOSTlocalhost
POSTGRES_PORT3306

We load the environment variables from the .env file and create a privileged configuration.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code, unused_variables)]

    use std::sync::OnceLock;

    // import privileged configuration
    use db_pool::PrivilegedPostgresConfig;
    // import dotenvy
    use dotenvy::dotenv;

    fn get_connection_pool() {
        static POOL: OnceLock<()> = OnceLock::new();

        let db_pool = POOL.get_or_init(|| {
            // load environment variables from .env
            dotenv().ok();

            // create privileged configuration from environment variables
            let config = PrivilegedPostgresConfig::from_env().unwrap();
        });
    }
}

PrivilegedPostgresConfig is the database connection configuration that includes username, password, host, and port. It is used for administration of the created databases - creation, cleaning, and dropping.

We then create the backend with the privileged configuration, among others.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code, unused_variables)]

    use std::sync::OnceLock;

    use db_pool::{
        // import backend
        sync::DieselPostgresBackend,
        PrivilegedPostgresConfig,
    };
    // import diesel-specific constructs
    use diesel::{sql_query, RunQueryDsl};
    use dotenvy::dotenv;
    // import connection pool
    use r2d2::Pool;

    fn get_connection_pool() {
        static POOL: OnceLock<()> = OnceLock::new();

        let db_pool = POOL.get_or_init(|| {
            dotenv().ok();

            let config = PrivilegedPostgresConfig::from_env().unwrap();

            // create backend
            let backend = DieselPostgresBackend::new(
                config,
                // create privileged connection pool with max 10 connections
                || Pool::builder().max_size(10),
                // create restricted connection pool with max 2 connections
                || Pool::builder().max_size(2),
                // create entities
                move |conn| {
                    sql_query("CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)")
                        .execute(conn)
                        .unwrap();
                },
            )
            .unwrap();
        });
    }
}

DieselPostgresBackend is the backend that will communicate with the PostgreSQL instance using Diesel-specific constructs.

PrivilegedPostgresConfig::from_env().unwrap() creates a privileged Postgres configuration from environment variables.

|| Pool::builder().max_size(10) creates a privileged connection pool with a max of 10 connections. This pool is used for administration of the created databases and relies on the privileged configuration to establish connections.

|| Pool::builder().max_size(2) creates a restricted connection pool meant to be used by a test. A restricted connection pool is created every time the database pool runs out of available connection pools to lend.

The last closure creates the database entities required to be available for tests. This happens every time a new restricted connection pool needs to be created - a new database is created, its entities are created, and the restricted connection pool is bound to it.

We create a database pool from the backend.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code, unused_variables)]

    use std::sync::OnceLock;

    use db_pool::{
        sync::{
            // import database pool
            DatabasePool,
            // import database pool builder trait
            DatabasePoolBuilderTrait,
            DieselPostgresBackend,
        },
        PrivilegedPostgresConfig,
    };
    use diesel::{sql_query, RunQueryDsl};
    use dotenvy::dotenv;
    use r2d2::Pool;

    fn get_connection_pool() {
        // change OnceLock inner type
        static POOL: OnceLock<DatabasePool<DieselPostgresBackend>> = OnceLock::new();

        let db_pool = POOL.get_or_init(|| {
            dotenv().ok();

            let config = PrivilegedPostgresConfig::from_env().unwrap();

            let backend = DieselPostgresBackend::new(
                config,
                || Pool::builder().max_size(10),
                || Pool::builder().max_size(2),
                move |conn| {
                    sql_query("CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)")
                        .execute(conn)
                        .unwrap();
                },
            )
            .unwrap();

            // create database pool
            backend.create_database_pool().unwrap()
        });
    }
}

DatabasePool is the returned pool of connection pools that will be assigned to tests in isolation. The connection pools can be reused after a test has finished and no longer needs the connection pool assigned to it.

We pull a connection pool out of the database pool.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code)]

    use std::sync::OnceLock;

    use db_pool::{
        sync::{
            DatabasePool,
            DatabasePoolBuilderTrait,
            DieselPostgresBackend,
            // import reusable connection pool
            ReusableConnectionPool,
        },
        PrivilegedPostgresConfig,
    };
    use diesel::{sql_query, RunQueryDsl};
    use dotenvy::dotenv;
    use r2d2::Pool;

    // change return type
    fn get_connection_pool() -> ReusableConnectionPool<'static, DieselPostgresBackend> {
        static POOL: OnceLock<DatabasePool<DieselPostgresBackend>> = OnceLock::new();

        let db_pool = POOL.get_or_init(|| {
            dotenv().ok();

            let config = PrivilegedPostgresConfig::from_env().unwrap();

            let backend = DieselPostgresBackend::new(
                config,
                || Pool::builder().max_size(10),
                || Pool::builder().max_size(2),
                move |conn| {
                    sql_query("CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)")
                        .execute(conn)
                        .unwrap();
                },
            )
            .unwrap();

            backend.create_database_pool().unwrap()
        });

        // pull connection pool
        db_pool.pull_immutable()
    }
}

ConnectionPool is a connection pool assigned to a test.

Reusable is a wrapper around ConnectionPool that allows access to the connection pool by the assigned test and orchestrates its return to the database pool after a test has finished and no longer needs it.

We add a simple test case that inserts a row then counts the number of rows in a table.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code)]

    use std::sync::OnceLock;

    use db_pool::{
        sync::{
            DatabasePool, DatabasePoolBuilderTrait, DieselPostgresBackend, ReusableConnectionPool,
        },
        PrivilegedPostgresConfig,
    };
    // import extra diesel-specific constructs
    use diesel::{insert_into, sql_query, table, Insertable, QueryDsl, RunQueryDsl};
    use dotenvy::dotenv;
    use r2d2::Pool;

    fn get_connection_pool() -> ReusableConnectionPool<'static, DieselPostgresBackend> {
        static POOL: OnceLock<DatabasePool<DieselPostgresBackend>> = OnceLock::new();

        let db_pool = POOL.get_or_init(|| {
            dotenv().ok();

            let config = PrivilegedPostgresConfig::from_env().unwrap();

            let backend = DieselPostgresBackend::new(
                config,
                || Pool::builder().max_size(10),
                || Pool::builder().max_size(2),
                move |conn| {
                    sql_query("CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)")
                        .execute(conn)
                        .unwrap();
                },
            )
            .unwrap();

            backend.create_database_pool().unwrap()
        });

        db_pool.pull_immutable()
    }

    // add test case
    fn test() {
        table! {
            book (id) {
                id -> Int4,
                title -> Text
            }
        }

        #[derive(Insertable)]
        #[diesel(table_name = book)]
        struct NewBook<'a> {
            title: &'a str,
        }

        // get connection pool from database pool
        let conn_pool = get_connection_pool();
        let conn = &mut conn_pool.get().unwrap();

        let new_book = NewBook { title: "Title" };

        insert_into(book::table)
            .values(&new_book)
            .execute(conn)
            .unwrap();

        let count = book::table.count().get_result::<i64>(conn).unwrap();
        assert_eq!(count, 1);
    }
}

The test gets a connection pool from the database pool every time it runs.

Finally, we add a couple of separate tests that call the same test case.

fn main() {}

#[cfg(test)]
mod tests {
    use std::sync::OnceLock;

    use db_pool::{
        sync::{
            DatabasePool, DatabasePoolBuilderTrait, DieselPostgresBackend, ReusableConnectionPool,
        },
        PrivilegedPostgresConfig,
    };
    use diesel::{insert_into, sql_query, table, Insertable, QueryDsl, RunQueryDsl};
    use dotenvy::dotenv;
    use r2d2::Pool;

    fn get_connection_pool() -> ReusableConnectionPool<'static, DieselPostgresBackend> {
        static POOL: OnceLock<DatabasePool<DieselPostgresBackend>> = OnceLock::new();

        let db_pool = POOL.get_or_init(|| {
            dotenv().ok();

            let config = PrivilegedPostgresConfig::from_env().unwrap();

            let backend = DieselPostgresBackend::new(
                config,
                || Pool::builder().max_size(10),
                || Pool::builder().max_size(2),
                move |conn| {
                    sql_query("CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)")
                        .execute(conn)
                        .unwrap();
                },
            )
            .unwrap();

            backend.create_database_pool().unwrap()
        });

        db_pool.pull_immutable()
    }

    fn test() {
        table! {
            book (id) {
                id -> Int4,
                title -> Text
            }
        }

        #[derive(Insertable)]
        #[diesel(table_name = book)]
        struct NewBook<'a> {
            title: &'a str,
        }

        let conn_pool = get_connection_pool();
        let conn = &mut conn_pool.get().unwrap();

        let new_book = NewBook { title: "Title" };

        insert_into(book::table)
            .values(&new_book)
            .execute(conn)
            .unwrap();

        let count = book::table.count().get_result::<i64>(conn).unwrap();
        assert_eq!(count, 1);
    }

    // add first test
    #[test]
    fn test1() {
        test();
    }

    // add second test
    #[test]
    fn test2() {
        test();
    }
}

We run the test cases in parallel and both pass.

running 2 tests
test tests::test1 ... ok
test tests::test2 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s

We will use the Diesel async Postgres backend with BB8 in this tutorial.

The database pool has to live for the duration of the test run. We use a OnceCell to store a "lazy static".

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code)]

    // import OnceCell
    use tokio::sync::OnceCell;

    async fn get_connection_pool() {
        // add "lazy static"
        static POOL: OnceCell<()> = OnceCell::const_new();
    }
}

We then need to create a privileged database configuration loaded from environment variables.

We create a .env file.

export POSTGRES_USERNAME=postgres
export POSTGRES_PASSWORD=postgres
export POSTGRES_HOST=localhost
export POSTGRES_PORT=3306

The environment variables used are optional.

Environment VariableDefault
POSTGRES_USERNAMEpostgres
POSTGRES_PASSWORD{blank}
POSTGRES_HOSTlocalhost
POSTGRES_PORT3306

We load the environment variables from the .env file and create a privileged configuration.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code, unused_variables)]

    // import privileged configuration
    use db_pool::PrivilegedPostgresConfig;
    // import dotenvy
    use dotenvy::dotenv;
    use tokio::sync::OnceCell;

    async fn get_connection_pool() {
        static POOL: OnceCell<()> = OnceCell::const_new();

        let db_pool = POOL
            .get_or_init(|| async {
                // load environment variables from .env
                dotenv().ok();

                // create privileged configuration from environment variables
                let config = PrivilegedPostgresConfig::from_env().unwrap();
            })
            .await;
    }
}

PrivilegedPostgresConfig is the database connection configuration that includes username, password, host, and port. It is used for administration of the created databases - creation, cleaning, and dropping.

We then create the backend with the privileged configuration, among others.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code, unused_variables)]

    // import connection pool
    use bb8::Pool;
    use db_pool::{
        // import backend
        r#async::{DieselAsyncPostgresBackend, DieselBb8},
        PrivilegedPostgresConfig,
    };
    // import diesel-specific constructs
    use diesel::sql_query;
    use diesel_async::RunQueryDsl;
    use dotenvy::dotenv;
    use tokio::sync::OnceCell;

    async fn get_connection_pool() {
        static POOL: OnceCell<()> = OnceCell::const_new();
        let db_pool = POOL
            .get_or_init(|| async {
                dotenv().ok();

                let config = PrivilegedPostgresConfig::from_env().unwrap();

                // create backend for BB8 connection pools
                let backend = DieselAsyncPostgresBackend::<DieselBb8>::new(
                    config,
                    // create privileged connection pool with max 10 connections
                    || Pool::builder().max_size(10),
                    // create restricted connection pool with max 2 connections
                    || Pool::builder().max_size(2),
                    // no custom create connection
                    None,
                    // create entities
                    move |mut conn| {
                        Box::pin(async {
                            sql_query(
                                "CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)",
                            )
                            .execute(&mut conn)
                            .await
                            .unwrap();

                            conn
                        })
                    },
                )
                .await
                .unwrap();
            })
            .await;
    }
}

DieselPostgresBackend is the backend that will communicate with the PostgreSQL instance using Diesel-specific constructs.

DieselBb8 is a construct that allows the backend to work with and issue BB8 connection pools.

PrivilegedPostgresConfig::from_env().unwrap() creates a privileged Postgres configuration from environment variables.

|| Pool::builder().max_size(10) creates a privileged connection pool with a max of 10 connections. This pool is used for administration of the created databases and relies on the privileged configuration to establish connections.

|| Pool::builder().max_size(2) creates a restricted connection pool meant to be used by a test. A restricted connection pool is created every time the database pool runs out of available connection pools to lend.

The last closure creates the database entities required to be available for tests. This happens every time a new restricted connection pool needs to be created - a new database is created, its entities are created, and the restricted connection pool is bound to it.

We create a database pool from the backend.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code, unused_variables)]

    use bb8::Pool;
    use db_pool::{
        r#async::{
            // import database pool
            DatabasePool,
            // import database pool builder trait
            DatabasePoolBuilderTrait,
            DieselAsyncPostgresBackend,
            DieselBb8,
        },
        PrivilegedPostgresConfig,
    };
    use diesel::sql_query;
    use diesel_async::RunQueryDsl;
    use dotenvy::dotenv;
    use tokio::sync::OnceCell;

    async fn get_connection_pool() {
        // change OnceCell inner type
        static POOL: OnceCell<DatabasePool<DieselAsyncPostgresBackend<DieselBb8>>> =
            OnceCell::const_new();

        let db_pool = POOL.get_or_init(|| async {
            dotenv().ok();

            let config = PrivilegedPostgresConfig::from_env().unwrap();

            // Diesel pool association type can be inferred now
            let backend = DieselAsyncPostgresBackend::new(
                config,
                || Pool::builder().max_size(10),
                || Pool::builder().max_size(2),
                None,
                move |mut conn| {
                    Box::pin(async {
                        sql_query("CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)")
                            .execute(&mut conn)
                            .await
                            .unwrap();

                        conn
                    })
                },
            )
            .await
            .unwrap();

            // create database pool
            backend.create_database_pool().await.unwrap()
        });
    }
}

DatabasePool is the returned pool of connection pools that will be assigned to tests in isolation. The connection pools can be reused after a test has finished and no longer needs the connection pool assigned to it.

We pull a connection pool out of the database pool.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code)]

    use bb8::Pool;
    use db_pool::{
        r#async::{
            DatabasePool,
            DatabasePoolBuilderTrait,
            DieselAsyncPostgresBackend,
            DieselBb8,
            // import reusable connection pool
            ReusableConnectionPool,
        },
        PrivilegedPostgresConfig,
    };
    use diesel::sql_query;
    use diesel_async::RunQueryDsl;
    use dotenvy::dotenv;
    use tokio::sync::OnceCell;

    // change return type
    async fn get_connection_pool(
    ) -> ReusableConnectionPool<'static, DieselAsyncPostgresBackend<DieselBb8>> {
        static POOL: OnceCell<DatabasePool<DieselAsyncPostgresBackend<DieselBb8>>> =
            OnceCell::const_new();

        let db_pool = POOL
            .get_or_init(|| async {
                dotenv().ok();

                let config = PrivilegedPostgresConfig::from_env().unwrap();

                let backend = DieselAsyncPostgresBackend::new(
                    config,
                    || Pool::builder().max_size(10),
                    || Pool::builder().max_size(2),
                    None,
                    move |mut conn| {
                        Box::pin(async {
                            sql_query(
                                "CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)",
                            )
                            .execute(&mut conn)
                            .await
                            .unwrap();

                            conn
                        })
                    },
                )
                .await
                .unwrap();

                backend.create_database_pool().await.unwrap()
            })
            .await;

        // pull connection pool
        db_pool.pull_immutable().await
    }
}

ConnectionPool is a connection pool assigned to a test.

Reusable is a wrapper around ConnectionPool that allows access to the connection pool by the assigned test and orchestrates its return to the database pool after a test has finished and no longer needs it.

We add a simple test case that inserts a row then counts the number of rows in a table.

fn main() {}

#[cfg(test)]
mod tests {
    #![allow(dead_code)]

    use bb8::Pool;
    use db_pool::{
        r#async::{
            DatabasePool, DatabasePoolBuilderTrait, DieselAsyncPostgresBackend, DieselBb8,
            ReusableConnectionPool,
        },
        PrivilegedPostgresConfig,
    };
    // import extra diesel-specific constructs
    use diesel::{insert_into, sql_query, table, Insertable, QueryDsl};
    use diesel_async::RunQueryDsl;
    use dotenvy::dotenv;
    use tokio::sync::OnceCell;

    async fn get_connection_pool(
    ) -> ReusableConnectionPool<'static, DieselAsyncPostgresBackend<DieselBb8>> {
        static POOL: OnceCell<DatabasePool<DieselAsyncPostgresBackend<DieselBb8>>> =
            OnceCell::const_new();

        let db_pool = POOL
            .get_or_init(|| async {
                dotenv().ok();

                let config = PrivilegedPostgresConfig::from_env().unwrap();

                let backend = DieselAsyncPostgresBackend::new(
                    config,
                    || Pool::builder().max_size(10),
                    || Pool::builder().max_size(2),
                    None,
                    move |mut conn| {
                        Box::pin(async {
                            sql_query(
                                "CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)",
                            )
                            .execute(&mut conn)
                            .await
                            .unwrap();

                            conn
                        })
                    },
                )
                .await
                .unwrap();

                backend.create_database_pool().await.unwrap()
            })
            .await;

        db_pool.pull_immutable().await
    }

    // add test case
    async fn test() {
        table! {
            book (id) {
                id -> Int4,
                title -> Text
            }
        }

        #[derive(Insertable)]
        #[diesel(table_name = book)]
        struct NewBook<'a> {
            title: &'a str,
        }

        // get connection pool from database pool
        let conn_pool = get_connection_pool().await;
        let conn = &mut conn_pool.get().await.unwrap();

        let new_book = NewBook { title: "Title" };

        insert_into(book::table)
            .values(&new_book)
            .execute(conn)
            .await
            .unwrap();

        let count = book::table.count().get_result::<i64>(conn).await.unwrap();
        assert_eq!(count, 1);
    }
}

The test gets a connection pool from the database pool every time it runs.

Finally, we add a couple of separate tests that call the same test case.

fn main() {}

#[cfg(test)]
mod tests {
    // allow needless returns generated by test macro
    #![allow(clippy::needless_return)]

    use bb8::Pool;
    use db_pool::{
        r#async::{
            DatabasePool, DatabasePoolBuilderTrait, DieselAsyncPostgresBackend, DieselBb8,
            ReusableConnectionPool,
        },
        PrivilegedPostgresConfig,
    };
    use diesel::{insert_into, sql_query, table, Insertable, QueryDsl};
    use diesel_async::RunQueryDsl;
    use dotenvy::dotenv;
    use tokio::sync::OnceCell;
    // import test macro
    use tokio_shared_rt::test;

    async fn get_connection_pool(
    ) -> ReusableConnectionPool<'static, DieselAsyncPostgresBackend<DieselBb8>> {
        static POOL: OnceCell<DatabasePool<DieselAsyncPostgresBackend<DieselBb8>>> =
            OnceCell::const_new();

        let db_pool = POOL
            .get_or_init(|| async {
                dotenv().ok();

                let config = PrivilegedPostgresConfig::from_env().unwrap();

                let backend = DieselAsyncPostgresBackend::new(
                    config,
                    || Pool::builder().max_size(10),
                    || Pool::builder().max_size(2),
                    None,
                    move |mut conn| {
                        Box::pin(async {
                            sql_query(
                                "CREATE TABLE book(id SERIAL PRIMARY KEY, title TEXT NOT NULL)",
                            )
                            .execute(&mut conn)
                            .await
                            .unwrap();

                            conn
                        })
                    },
                )
                .await
                .unwrap();

                backend.create_database_pool().await.unwrap()
            })
            .await;

        db_pool.pull_immutable().await
    }

    async fn test() {
        table! {
            book (id) {
                id -> Int4,
                title -> Text
            }
        }

        #[derive(Insertable)]
        #[diesel(table_name = book)]
        struct NewBook<'a> {
            title: &'a str,
        }

        let conn_pool = get_connection_pool().await;
        let conn = &mut conn_pool.get().await.unwrap();

        let new_book = NewBook { title: "Title" };

        insert_into(book::table)
            .values(&new_book)
            .execute(conn)
            .await
            .unwrap();

        let count = book::table.count().get_result::<i64>(conn).await.unwrap();
        assert_eq!(count, 1);
    }

    // add first test
    #[test(shared)]
    async fn test1() {
        test().await;
    }

    // add second test
    #[test(shared)]
    async fn test2() {
        test().await;
    }
}

We use the test macro from tokio_shared_rt instead of tokio::test in order to use a shared Tokio runtime for all tests. This is essential so that the static database pool is created and used by the same runtime and therefore remains valid for all tests.

We run the test cases in parallel and both pass.

running 2 tests
test tests::test1 ... ok
test tests::test2 ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s