[−][src]Crate diesel_pg_tester
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.)
Structs
DieselPgTester | Diesel test runner. |