#postgresql #diesel #testing #once-cell

diesel_pg_tester

Runs Diesel tests efficiently using a live Postgres connection

2 releases

0.5.1 Mar 1, 2020
0.5.0 Mar 1, 2020

#13 in #once-cell

MIT/Apache

19KB
229 lines

Runs diesel tests efficiently using a live Postgres connection.

MIT licensed Docs


lib.rs:

Runs diesel tests efficiently using a live Postgres connection.

This test runner takes advantage of the highly transactional nature of Postgres to quickly restore the database to a well-known state before each test. Once the database schema is initialized, it can take only a fraction of a second to run many tests.

Example

This example test module starts with an empty database. The insert_and_query_user() test uses a global variable called PGTEST to run code in a live Postgres transaction. The init_db() function could read the schema from a file or use Diesel migration to initialize the schema.

#[macro_use] extern crate diesel;
use diesel::connection::SimpleConnection;
use diesel::prelude::*;
use diesel::sql_types::*;
use diesel_pg_tester::DieselPgTester;
use once_cell::sync::Lazy;

// TestResult is the type that tests and init_db() return. Any error type is acceptable, but
// diesel::result::Error is convenient for this example.
type TestResult<T = ()> = Result<T, diesel::result::Error>;

// PGTEST is the global DieselPgTester that the whole test suite will use.
// The error type argument must match the TestResult error type.
static PGTEST: Lazy<DieselPgTester<diesel::result::Error>> =
    Lazy::new(|| DieselPgTester::start(1, None, init_db));

// init_db() initializes the temporary schema in the database.
fn init_db(conn: &PgConnection) -> TestResult {
    conn.batch_execute(
        "CREATE TABLE users (id BIGSERIAL PRIMARY KEY NOT NULL, name VARCHAR);")?;
    Ok(())
}

// This table! macro invocation generates a helpful submodule called users::dsl.
table! {
    users (id) {
        id -> Int8,
        name -> Varchar,
    }
}

// insert_and_query_user() is a sample test that uses PGTEST and the users::dsl submodule.
#[test]
fn insert_and_query_user() -> TestResult {
    PGTEST.run(|conn| {
        use users::dsl as U;
        diesel::insert_into(U::users).values(&U::name.eq("Quentin")).execute(conn)?;
        let user: (i64, String) = U::users.first(conn)?;
        assert_eq!("Quentin", user.1);
        Ok(())
    })
}

How It Works

Tests are run as follows:

  • A test suite creates a global DieselPgTester, which starts one or more worker threads.

  • The test suite adds tests to the DieselPgTester's queue.

  • Each worker thread opens a connection to Postgres and starts a transaction.

  • Inside the transaction, the worker creates a temporary schema and runs an initialization function provided by the test suite to create the tables and other objects needed by the code to be tested.

  • Once the schema is set up, the worker polls the queue for a test to run.

  • The worker creates a savepoint.

  • The worker runs a test, reporting the test result back to the thread that queued the test.

  • After the test, the worker restores the savepoint created before the test, which reverts the schema and data to its state before the test.

  • The worker repeatedly polls the queue and runs tests until the process ends, a test panics, or the DieselPgTester is dropped.

  • Because the transaction is never committed, the database is left in its original state for later test runs.

During development, tests often panic. When a test running in DieselPgTester panics, DieselPgTester chooses the safe route: it drops the database connection, the transaction is aborted, and the worker detects the panic and starts another worker to replace itself. If there is only one worker, there will be a pause after every panicked test as a new worker connects to the database and re-initializes. To avoid this pause, tests can choose to return Result::Err rather than panic, allowing the worker to clean up normally and quickly rather than drop the connection.

If the database initialization fails or causes a panic, the DieselPgTester is halted to avoid wasting time running tests that will ultimately fail. All tests running through a halted DieselPgTester fail quickly.

Usage Notes

  • Nothing should ever commit the transaction during testing. It is possible to commit the transaction using a manually emitted COMMIT statement, but doing so will likely make the test suite unreliable.

  • Even in Postgres, some database state (such as sequence values) is intentionally non-transactional. Avoid making your tests dependent on non-transactional state.

  • If the database schema depends on Postgres extensions or other features that must be set up by a database superuser, those features need to be set up before running tests.

  • The default Postgres schema is called public, but DieselPgTester creates and uses a temporary schema rather than the public schema, so most SQL should not specify a schema name. Diesel doesn't normally generate SQL with schema names, but the pg_dump utility does, so you should strip out the public. prefix from SQL generated by pg_dump (except when using Postgres extensions that depend on the public schema.)

Dependencies

~5MB
~97K SLoC