effect_test — The Test Harness Boundary
Effectful tests should compose effects in the test body and let the harness execute them. This keeps lazy execution semantics intact and keeps direct Effect::run(&mut ...) calls inside runtime/test harness internals.
Preferred Usage
use effectful::{Effect, effect_test};
#[effect_test]
fn simple_effect_succeeds() -> Effect<(), &'static str, ()> {
Effect::new(|_| Ok(()))
}
#[effect_test] creates an async Tokio test and runs the returned effect. Ok(_) passes. Err(E) panics with Debug output, so the error type must implement Debug.
The macro uses effectful’s internal current-thread Tokio test re-export; downstream crates do not need to call Effect::run(&mut ...) in the test body.
Provided ServiceContext
use effectful::{Effect, MissingService, Service, ServiceContext, effect_test};
#[derive(Clone, Service)]
struct Config {
port: u16,
}
fn test_env() -> ServiceContext {
Config { port: 8080 }.to_context()
}
#[effect_test(env = "test_env")]
fn reads_config() -> Effect<(), MissingService, ServiceContext> {
Effect::<Config, MissingService, ServiceContext>::service::<Config>()
.map(|config| assert_eq!(config.port, 8080))
}
The fixture runs once per test and returns the environment consumed by the effect.
Layer-Based Setup
use effectful::{Effect, Layer, MissingService, Service, ServiceContext, effect_test};
#[derive(Clone, Service)]
struct Config {
port: u16,
}
fn test_layer() -> Layer<Config, MissingService, ()> {
Layer::succeed(Config { port: 8080 })
}
#[effect_test(layer = "test_layer")]
fn reads_layer_config() -> Effect<(), MissingService, ServiceContext> {
Effect::<Config, MissingService, ServiceContext>::service::<Config>()
.map(|config| assert_eq!(config.port, 8080))
}
This is the Rust equivalent of Effect’s it.layer(...) boundary: the test body remains an effect, and the adapter builds/provides the services.
Helper API
Use helper functions when an attribute macro is not appropriate.
use effectful::testing::expect_effect_test;
#[tokio::test]
async fn create_user_inserts_into_db() {
expect_effect_test(create_user(NewUser { name: "Alice".into(), age: 30 })).await;
}
Available helpers:
| Function | Notes |
|---|---|
expect_effect_test(effect).await | Run with R: Default, panic on Err(E: Debug) |
expect_effect_test_with_env(effect, env).await | Run with explicit environment, panic on failure |
expect_effect_test_with_layer(effect, layer).await | Build a layer for ServiceContext, panic on failure |
run_effect_test(effect).await | Run with R: Default, return Result<A, E> |
run_effect_test_with_env(effect, env).await | Run with explicit environment, return Result<A, E> |
TestRuntime::with_env(fixture) | Reusable adapter for explicit fixture functions |
Asserting on Exit
run_test remains available when you need the older synchronous Exit<A, E> shape.
use effectful::{Exit, run_test};
#[test]
fn division_by_zero_fails() {
let exit = run_test(divide(10, 0), ());
assert!(matches!(exit, Exit::Failure(Cause::Fail(DivError::DivisionByZero))));
}
Pass () for effects with no environment. run_test(effect, env) resets the test leak counters, runs the effect with run_blocking, then checks the leak counters.
Common Exit shapes:
| Exit | Meaning |
|---|---|
Exit::Success(a) | Effect succeeded |
Exit::Failure(Cause::Fail(e)) | Typed failure |
Exit::Failure(Cause::Die(message)) | Defect message |
Exit::Failure(Cause::Interrupt(id)) | Fiber interrupt |
run_test_with_clock
use std::time::Instant;
use effectful::{TestClock, run_test_with_clock};
let clock = TestClock::new(Instant::now());
let exit = run_test_with_clock(effect, env, clock);
run_test_with_clock currently delegates to run_test after accepting the explicit clock argument. Use explicit-clock scheduling helpers (retry_with_clock, repeat_with_clock) when the effect itself must use that clock.
Leak Assertions
The testing module exposes assertion effects and test hooks:
use effectful::{assert_no_leaked_fibers, assert_no_unclosed_scopes};
run_blocking(assert_no_leaked_fibers(), ())?;
run_blocking(assert_no_unclosed_scopes(), ())?;
The effect-test helpers and run_test call both assertions after the effect run. If a hook recorded a leak, the assertion panics.