2 releases
0.5.1 | Mar 1, 2020 |
---|---|
0.5.0 | Mar 1, 2020 |
#14 in #once-cell
19KB
229 lines
Runs diesel
tests efficiently using a live Postgres connection.
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
, butDieselPgTester
creates and uses a temporary schema rather than thepublic
schema, so most SQL should not specify a schema name. Diesel doesn't normally generate SQL with schema names, but thepg_dump
utility does, so you should strip out thepublic.
prefix from SQL generated bypg_dump
(except when using Postgres extensions that depend on thepublic
schema.)
Dependencies
~5.5MB
~104K SLoC