Testing
Riverbase removed its in-memory backends on purpose: tests run against real Postgres, so they exercise the same transactions, constraints, and policy filters as production.
Layers
| Layer | What to test | Needs Postgres |
|---|---|---|
| Pure functions | Validation helpers, mappers, row conversions | no |
| Static checks | registered_event_keys() coverage, sort coverage | no |
| Engine | Commands and queries through typed helpers | yes |
| HTTP | Routes, auth, Casbin, and envelopes through the full stack | yes |
A Postgres for tests
docker run -d --name riverbase-test-pg -p 55432:5432 \
-e POSTGRES_USER=riverbase -e POSTGRES_PASSWORD=test -e POSTGRES_DB=riverbase_test \
postgres:16-alpine
export RIVERBASE_DB_URL=postgres://riverbase:test@127.0.0.1:55432/riverbase_test
Use a dedicated test database. Tests create rows, and the framework migrations run on first connect.
Tests that need a database should skip cleanly when none is configured:
async fn pool_or_skip() -> Option<PgPool> {
let url = std::env::var("RIVERBASE_DB_URL").or_else(|_| std::env::var("DB_URL")).ok()?;
riverbase_core::datastore::postgres::establish_dbpool(&url).await.ok()
}
#[tokio::test]
async fn creates_a_todo() {
let Some(pool) = pool_or_skip().await else { return };
// …
}
In CI, a skipped database test hides a broken setup. Fail instead of skipping when CI (or a
variable of your own, such as RIVERBASE_TEST_REQUIRE_DB) is set.
Static checks worth having
#[test]
fn domain_action_keys_are_registered() {
let missing = riverbase_core::missing_action_event_keys(
include_str!("aggregate.rs"),
TodoAggregate::<PgDataStore>::registered_event_keys(),
);
assert!(missing.is_empty(), "unregistered #[domain_action] keys: {missing:?}");
}
Engine tests
Spawn the domain against a DomainRuntime and call the typed helpers that command_engine!
generated:
let runtime = DomainRuntime::from_config(&RiverbaseConfig::load()?).await?;
let domain = TodoDomain::<PgDataStore>::spawn(&runtime).await?;
let created = domain.command().create("Buy milk").await?;
let id = created["id"].as_str().unwrap();
domain.command().set_done(id, true).await?; // helpers come from the `methods ( … )` block
End-to-end HTTP tests
Drive the coupled API end to end in a tests/ file: create, update, list, and item. Build the
router with RiverbaseApp::from_runtime(…) and call it in-process, or start the binary and use an
HTTP client. For authenticated routes, send a MockAuth- header with the roles under
test, and assert both the allowed and the denied path.
Policies
Test Casbin policy without HTTP:
let authz = riverbase_http::casbin::build_authorizer(pool, None, Some(POLICY_CSV)).await?;
let allowed = DomainActivity::new("riverbase.todo", "create-todo.execute").with_resource("todo");
authz.enforce(&ActivityRequest::new("alice", allowed)).await?;
assert!(authz.enforce(&ActivityRequest::new("bob", denied)).await.is_err());
Running
cargo test --workspace # everything
cargo test -p todo-service --test http # one integration-test file
cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings