navree

Getting started

Test a Navree app the way it runs: TestApp boots your modules in-process, with a fresh SQLite database per test, and cargo test runs it all.

On this page

Navree tests exercise your application the way it runs in production. A test boots your real modules in-process, sends requests through the full pipeline (request id, CORS, authentication, access checks, the unit of work, the error format) and reads the results from a real database. Only the edges you choose are faked: outgoing mail, the queue, the clock, outgoing HTTP.

This is the approach of Laravel's feature tests, and it keeps tests honest: a test that passes has gone through the same router, extractors and transactions as a real request. It also stays fast, because there is no server process and no network, and the database is a SQLite file in a temporary directory.

The testing kit

The kit is the navree-testing crate, available as navree::testing with the testing feature (part of full, which is on by default). It isn't in the prelude, so import what you use:

Rust
NameWhat it is
TestAppA booted app for one test.
TestClient, TestRequest, TestResponseIn-process requests and assertions; see HTTP tests.
principal(user_id)A test user to act as.
TestAuthenticatorThe authenticator behind acting_as.
temp_sqlite_url(), TempDirGuardA fresh SQLite database file and the guard that deletes it.
FixedClockA clock that stands still until you move it; see fakes.

Tests are async, so your app needs tokio as a dev-dependency. An app made by navree new has it:

Cargo.toml

Your first test

This test boots the deals module (and the companies module its tables point at) with a database, and checks that a signed-in user can list deals while a guest can't. Its list route is #[authenticated]; a route with #[can(..)] also needs the access module and some abilities, as in testing access:

src/modules/deals/tests/mod.rs

Each test builds its own app, so tests share nothing and cargo test runs them in parallel.

Where to put tests:

  • Module tests live next to the module, in src/modules/<module>/tests/, declared with #[cfg(test)] mod tests;. navree make:module deals --crud writes a set of them, and navree make:test deals moving_deals adds a file with a test that boots the module.
  • App tests live in the top-level tests/ directory and boot the whole app from its root (below). A new app has tests/app.rs.

Building a test app

TestApp::builder() assembles an app from:

  1. ConfigModule, configured with no sources: a test never reads .env, config/app.toml or APP_* environment variables, so a developer's settings can't change its result;
  2. HttpModule;
  3. the modules you add, in order.
MethodWhat it does
.module(m)Adds a module. A configured HttpModule::for_root(..) replaces the default one.
.config(key, value)Sets a configuration key (a dotted path) to a JSON value, above every other source. Later calls win.
.sources(sources)Replaces the base configuration sources (none by default); .config values still apply on top.
.temp_database()Points database.url at a new SQLite file and sets database.migrate_on_boot = true.
.override_value::<T>(Arc<T>)Replaces the provider of T everywhere, for fakes.
.fake_auth()Authenticates with the TestAuthenticator even when the app has its own; see HTTP tests.
.build().awaitBoots the app, panicking with the boot error if it fails.
.try_build().awaitBoots the app and returns the boot error instead.

temp_database() sets the configuration only: add DatabaseModule yourself, or a module that imports it. Don't add a ConfigModule of your own, because the builder has configured one already and the boot would fail.

Once built, a TestApp gives you:

MethodWhat it does
app.client()A client for a guest, with an empty cookie jar.
app.acting_as(principal)A client whose requests are authenticated as principal.
app.get::<T>().awaitResolves a provider: app.get::<Db>(), app.get::<DealService>(). Panics if there is none.
app.ctx()The AppContext.
app.run_command(name, args).awaitRuns a registered command, such as db:seed, against this app.
app.shutdown().awaitShuts the app down gracefully and deletes its temporary database.

Booting your whole app

To test the app as main runs it, build its root with hermetic configuration and boot it with TestApp::from_builder. This is the tests/app.rs that navree new writes, without its /health/live test (my_app is your crate's name):

tests/app.rs

from_builder adds nothing to your app except a TestAuthenticator when the app has no authenticator of its own. A new app has navree-auth's, so the test installs the TestAuthenticator in its place to make acting_as work (apps with navree-auth). Keep the TempDirGuard alive for the whole test: dropping it deletes the database. validate() checks the module graph (imports, providers, exports) without building anything, so a wiring mistake fails one fast test with a readable message.

The settings above make the app hermetic and fast:

KeyWhy
app.keyEncryption and signing need a key; generate_key() makes a fresh one.
database.url, database.migrate_on_bootA private, migrated database.
http.per_ip_rate_limit = nullEvery test request comes from 127.0.0.1, and the default limit is 600 requests a minute per IP.
mail.transport = "array"Mail is kept in memory, to assert on.
observe.metrics = falseSkips the Prometheus recorder.
hashing.memory_kib, hashing.iterationsArgon2id with production settings is slow on purpose; cheap settings keep logins in tests fast.

The test database

temp_database() and temp_sqlite_url() create a new directory in the system's temporary directory (named navree-test-...) and point the app at a database.sqlite file inside it. SQLite creates the file on first connect, and migrate_on_boot runs every module's migrations, so each test starts from an empty, migrated schema. The directory is deleted when the TestApp (or the guard) is dropped.

A file is used rather than an in-memory database because an in-memory SQLite database belongs to one connection: every connection in the pool would see a different, empty database.

Tests run on SQLite only: there is no helper that creates a PostgreSQL database per test. Database tests covers preparing data and asserting on it.

Running tests

Shell

#[tokio::test] runs each test on a single-threaded runtime, which is right for nearly every test. A test about concurrency (many requests at once) needs real threads:

Rust

Popular pages

  • Installation
  • Your first app
  • Routing and controllers
  • Validation
  • Authentication
  • Benchmarks