navree
Documentation

Testing

Database assertions on rows and records, counting queries, #[navree::test] with a database of its own per test, templates built once and copied, real PostgreSQL and MySQL servers, and seeding test apps.

On this page

Tests of code that uses models need two things: a database of their own, so tests run in parallel without seeing each other's rows, and a way to say what the database should hold afterwards. The ORM's testing helpers live in navree::orm::testing, with the test databases under the testing feature (on in full).

src/modules/deals/tests/mod.rs

For tests that boot the whole app and send requests, TestApp gives each test a database too: see database tests. The assertions on this page work there as well, on the app's Db (app.get::<Db>().await?).

Database assertions

The assertions are async and panic with a report when they fail: what was expected, and the rows that came closest.

AssertionPasses when
assert_database_has(exec, table, conditions)a row matches every condition
assert_database_missing(exec, table, conditions)no row matches
assert_database_count(exec, table, n)the table has n rows
assert_database_empty(exec, table)the table has none
assert_model_exists(exec, &record), assert_model_missing(exec, &record)the record's row exists (or doesn't), ignoring soft deletes
assert_soft_deleted(exec, &record), assert_not_soft_deleted(exec, &record)the record's row is (or isn't) trashed
assert_soft_deleted_in(exec, table, conditions), assert_not_soft_deleted_in(..)the same, for rows found by conditions

Conditions are pairs ([("title", "Renewal")]), typed assignments ([Deal::STAGE.set(Stage::Won)], encoded with the column's casts, so a string enum compares with 'won'), or a Filter the database evaluates (Deal::AMOUNT_CENTS.gt(100_000)). Values compare as the column stores them: numbers by value whatever their width, booleans stored as integers, date-times by instant, JSON by structure (json_value(..) builds a JSON condition).

Rust

Counting queries

A test can pin how many statements a piece of code runs, which catches N+1 queries before production does:

Rust

Statements count at the executors, while the future's task runs them. With the strict modes on, as they are in tests by default, lazy loading inside a loop already fails; a query count also catches extra queries of other kinds.

A database per test

#[navree::test] runs an async fn test on a runtime of its own (no #[tokio::test] needed) and, when it takes a TestDb, gives it a database of its own:

Rust

database is "sqlite" (the default), "memory", "postgres" or "mysql". A test may return a Result. db.db() is the Db to pass to models; db.url() the URL, for booting an app on the same database.

Templates

Building the schema for every test is slow, so build it once: a Template runs its build function the first time a test asks for a copy, and every test then gets a copy of the result, schema and reference rows included.

Rust

The build function can also boot your app on template.url() with database.migrate_on_boot, to run your real migrations once. A copy is a copied file on SQLite, CREATE DATABASE .. TEMPLATE on PostgreSQL, and a new database with the template's tables and rows on MySQL. DATABASE.sqlite(), .postgres() and .mysql() hand out copies outside the attribute.

Real servers

Tests on PostgreSQL (with TimescaleDB) and MySQL read the server from NAVREE_TEST_POSTGRES_URL and NAVREE_TEST_MYSQL_URL (NAVREE_TEST_MONGODB_URL for MongoDB), or from target/test-databases.env when the variables are unset. A test whose server isn't configured returns early with a notice, so the same suite runs on a laptop without servers and in CI with them. The test binary needs navree-db's driver for the server: the facade's postgres or mysql feature.

Each test works in a database of its own, named navree_test_.., dropped when its TestDb is dropped, also when the test panics; databases a crashed run left behind are dropped by a later template build once they are six hours old.

Other strategies

For suites that share one database:

  • db.transaction(|uow| async move { .. }) runs the test in a unit of work and rolls it back afterwards. Its change observers and after-commit hooks still run, as on a commit. Requests a test sends commit units of work of their own, so give HTTP tests a copy instead.
  • db.truncate(&["navree_migrations"]) empties every table but the ones named, resetting key counters.
  • LazyTestDb creates the database the first time a test uses it, so tests that never touch it don't pay for it.

TestDb::sqlite(), TestDb::memory(), TestDb::postgres() and TestDb::mysql() give an empty database without a template. For an app booted with TestApp, memory_database() uses an in-memory SQLite database.

Seeding test apps

TestAppExt runs seeders on a booted TestApp:

Rust

Model events in tests

To check which model events fired without running their listeners, put a ModelEventFake in the test app: see events and observers.