context!() { /* proc-macro */ }Expand description
Declares a test context with multiple services.
Supports optional inline configuration for each service using the syntax:
service_name: ServiceType = config_expr
§Examples
§With inline configuration (recommended for different configs)
ⓘ
use admixture::context;
use testcontainers_modules::postgres::Postgres;
context! {
MyTestContext {
primary_db: SqlxPostgresServiceSetup = Postgres::default().with_tag("15"),
replica_db: SqlxPostgresServiceSetup = Postgres::default().with_tag("14"),
redis: RedisServiceSetup = Redis::default(),
}
}§Without inline configuration (uses Config::default())
ⓘ
use admixture::context;
context! {
MyTestContext {
postgres: SqlxPostgresServiceSetup,
redis: RedisServiceSetup,
}
}
// Requires that each service's Config type implements Default§Lifecycle Hooks
Contexts can define optional lifecycle hooks that run at specific points
in the test lifecycle. Hooks are defined in a separate hooks { ... } block
and can be placed in any order relative to service definitions.
ⓘ
use admixture::context;
use std::error::Error;
context! {
TestContext {
postgres: PostgresServiceSetup,
},
hooks {
before_all = setup_test_data,
after_all = cleanup_test_data,
before_each = reset_database,
after_each = verify_invariants,
}
}
async fn setup_test_data(ctx: &TestContextRunning) -> Result<(), Box<dyn Error + Send>> {
// Runs once after context starts, before any tests
let client = ctx.postgres().client().await?;
client.execute("INSERT INTO users (name) VALUES ('test')", &[]).await?;
Ok(())
}
async fn reset_database(ctx: &TestContextRunning) -> Result<(), Box<dyn Error + Send>> {
// Runs before each test
ctx.postgres().client().await?.execute("TRUNCATE users", &[]).await?;
Ok(())
}
async fn verify_invariants(ctx: &TestContextRunning) -> Result<(), Box<dyn Error + Send>> {
// Runs after each test (even if test failed)
let count: i64 = ctx.postgres().client().await?
.query_one("SELECT COUNT(*) FROM users WHERE invalid = true", &[])
.await?
.get(0);
if count > 0 {
return Err("Found invalid users after test".into());
}
Ok(())
}
async fn cleanup_test_data(ctx: &TestContextRunning) -> Result<(), Box<dyn Error + Send>> {
// Runs once after all tests, before context stops (best-effort)
ctx.postgres().client().await?.execute("DROP TABLE IF EXISTS temp_data", &[]).await?;
Ok(())
}§Hook Execution Order
- Context starts
- before_all - If this fails, all tests in the context group fail
- For each test:
- before_each - If this fails, that specific test fails
- Test runs
- after_each - If this fails, that test fails (always runs, even if test failed)
- after_all - Failure is logged but doesn’t fail tests (best-effort cleanup)
- Context stops
§Hook Failure Behavior
before_allfailure → all tests in context group failbefore_eachfailure → that specific test failsafter_eachfailure → that specific test failsafter_allfailure → logged as warning, doesn’t fail tests
§Hook Function Signature
All hook functions must have this signature:
ⓘ
async fn hook_name(ctx: &ContextNameRunning) -> Result<(), Box<dyn Error + Send>>§Flexible Ordering
Both services and hooks can appear in any order:
ⓘ
context! {
MyContext {
hooks {
before_each = reset_state,
},
postgres: PostgresServiceSetup,
hooks { // Can even split hooks if needed (though not recommended)
after_each = verify_state,
},
redis: RedisServiceSetup,
}
}This generates:
- A
MyTestContextConfigstruct with service config fields - A
MyTestContextSetupstruct with service setup fields - A
MyTestContextRunningstruct with running service fields - Implementations of
ContextSetupandContextRunningtraits - A type alias
MyTestContext = TestContext<MyTestContextRunning> - A constructor method
MyTestContext::new(setup) - A static
MYTESTCONTEXT_HOOKScontaining hook function pointers