Chuyển tới nội dung chính

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​

LayerWhat to testNeeds Postgres
Pure functionsValidation helpers, mappers, row conversionsno
Static checksregistered_event_keys() coverage, sort coverageno
EngineCommands and queries through typed helpersyes
HTTPRoutes, auth, Casbin, and envelopes through the full stackyes

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