Fixtures¶
Fixtures provide dependency-injected values to test functions. Define a fixture with #[skuld::fixture] and inject it into a test with #[fixture] on a parameter.
Basic usage¶
use std::path::Path;
#[skuld::test]
fn my_test(#[fixture(temp_dir)] dir: &Path) {
assert!(dir.exists());
}
The #[fixture(temp_dir)] annotation tells skuld to look up the fixture named temp_dir and inject it as the parameter’s value. If the parameter name matches the fixture name, a bare #[fixture] suffices:
#[skuld::test]
fn my_test(#[fixture] temp_dir: &Path) {
// Same as #[fixture(temp_dir)] — parameter name matches fixture name.
}
Defining fixtures¶
A fixture is a function annotated with #[skuld::fixture] that returns Result<T, String>:
pub struct MyResource { /* ... */ }
#[skuld::fixture]
fn my_resource() -> Result<MyResource, String> {
MyResource::create().map_err(|e| format!("setup failed: {e}"))
}
The #[skuld::fixture] attribute supports these options:
Option |
Description |
|---|---|
|
Lifetime scope (default: |
|
Runtime preconditions (propagated to tests) |
|
Override the fixture name (default: function name) |
|
Also support injection as |
|
Tests using this fixture inherit the serial constraint (see Serial Tests) |
Scopes¶
Each fixture has a lifetime scope that controls when it’s created and destroyed:
Scope |
Behaviour |
|---|---|
|
Fresh instance per injection. Dropped when the |
|
Cached per test. Dropped when the test ends. |
|
Cached globally. Dropped after all tests finish (LIFO). |
#[skuld::fixture(scope = test)]
fn db_connection() -> Result<DbConn, String> { /* ... */ }
#[skuld::fixture(scope = process, requires = [docker_available])]
fn corpus_image() -> Result<CorpusImage, String> { /* ... */ }
Scope dependency rule: a fixture may only depend on fixtures of the same or wider scope (variable < test < process). Dependency cycles are detected at startup.
Fixture dependencies¶
Fixtures can depend on other fixtures using the same #[fixture] parameter syntax:
#[skuld::fixture(scope = test, deref)]
fn test_name() -> Result<TestName, String> { /* ... */ }
#[skuld::fixture(deref)]
fn temp_dir(#[fixture(test_name)] name: &str) -> Result<TempDir, String> {
// `name` is injected from the test_name fixture.
tempfile::Builder::new()
.prefix(&format!("{name}-"))
.tempdir()
.map(|inner| TempDir { inner })
.map_err(|e| format!("failed to create temp dir: {e}"))
}
Deref coercion¶
Fixtures annotated with deref can be injected as either their own type or their Deref::Target:
// TempDir implements Deref<Target = Path>, so both work:
fn example1(#[fixture(temp_dir)] dir: &skuld::TempDir) { /* ... */ }
fn example2(#[fixture(temp_dir)] dir: &Path) { /* ... */ }
Requirement propagation¶
If a fixture declares requires = [...], any test using that fixture automatically inherits those requirements — even without listing them in the test’s own requires. This is transitive: if fixture A depends on fixture B which has requires = [docker], a test using fixture A will also require docker.
Built-in fixtures¶
Fixture |
Scope |
Type |
Serial |
Description |
|---|---|---|---|---|
|
test |
|
no |
Current test function name |
|
variable |
|
no |
Temporary directory named after the test |
|
test |
|
yes |
Set/remove env vars with automatic revert |
|
test |
|
yes |
Change working directory with automatic revert |
env — environment variables¶
The env fixture provides an EnvGuard for safely modifying environment variables. All changes are reverted when the test ends:
#[skuld::test]
fn my_test(#[fixture] env: &skuld::EnvGuard) {
env.set("DATABASE_URL", "sqlite::memory:");
env.remove("PROD_SECRET");
// Both changes are automatically reverted after the test.
}
Because environment variables are process-global, the env fixture is marked serial — tests using it never run in parallel.
cwd — working directory¶
The cwd fixture provides a CwdGuard for safely changing the working directory. It maintains a stack of directories, so back() returns to the previous one (like cd -):
#[skuld::test]
fn my_test(#[fixture] cwd: &skuld::CwdGuard, #[fixture(temp_dir)] dir: &Path) {
cwd.set(dir);
assert_eq!(std::env::current_dir().unwrap(), dir);
cwd.back(); // Return to the previous directory.
// The original directory is restored when the test ends regardless.
}
Like env, the cwd fixture is serial.
Serial fixtures¶
Fixtures support the same serial syntax as tests. A bare serial means serial with everything; serial = <expr> applies a filter:
#[skuld::label] const DATABASE: skuld::Label;
#[skuld::fixture(scope = test, serial = DATABASE)]
fn db_conn() -> Result<DbConn, String> {
Ok(DbConn::new())
}
Any test using db_conn inherits serial = DATABASE without declaring it. When multiple fixtures contribute different serial filters, they are combined with OR. See Serial Tests for details.
Tool fixtures¶
Fixtures like env and cwd are “tool fixtures” — instead of returning a value to read, they return an object with methods that the test calls. This pattern is useful for any fixture that needs test-specific arguments:
#[skuld::fixture(scope = test, serial)]
fn env() -> Result<EnvGuard, String> {
Ok(EnvGuard::new()) // Returns a tool, not a value.
}
#[skuld::test]
fn my_test(#[fixture] env: &EnvGuard) {
env.set("KEY", "value"); // Test provides "parameters" as method calls.
}
Eager initialization¶
Process-scoped fixtures can be pre-initialized before tests run using warm_up:
fn main() {
skuld::warm_up("corpus_image"); // Initialize now, not on first use.
skuld::run_all();
}