Fixtures and hooks
Dependency-injected fixtures with three scopes and automatic LIFO teardown. Hooks attach to the suite or test lifecycle.
Built-in fixtures
Scope hierarchy
pool.get::<T>("name") walks the scope chain, resolves dependencies
recursively, caches values, and registers teardown. The DAG is validated
at startup.
Hooks
All hook macros (#[before_all], #[after_all], #[before_each],
#[after_each]) take no attributes. Every hook receives a
TestContext — name it however you like. Suite hooks (before_all /
after_all) run once per suite per worker; each hooks (before_each /
after_each) run for every test.
Per-test lifecycle
Custom fixtures
Use the #[fixture] attribute to register a custom value. The body takes
a TestContext, returns ferridriver_test::Result<T>, and the value is
shared as Arc<T>. Retrieve it from a test with ctx.get::<T>("name").
scope is "test" (default), "worker", or "global"; add auto to
resolve the fixture for every test in scope, and timeout = "10s" to
bound setup. Fixtures can depend on built-in or other custom fixtures —
resolve them lazily with ctx.get inside the body. The fixture DAG is
validated at startup: a cycle aborts the run before any test starts.
Hooks vs fixtures
Both run around tests; they solve different problems:
- Fixtures are pull-based. The test asks for what it needs; unused fixtures never run. They carry values.
- Hooks are push-based. They run for every test in the suite whether the test uses them or not. They carry side effects.
If you have a value to inject, make it a fixture. If you have a side effect that every test needs regardless of the body (metrics tagging, screenshot on failure, log-capture setup), make it a hook.
Practical guidance
- Prefer test-scope over worker-scope. If a fixture is cheap (tens of ms), recreate it. You save a class of "why did this test pollute the next one" bugs.
- Don't hide
ctx.page()behind a fixture.pageis already a test-scoped built-in; a custom one would just be an alias. - Worker-scope is for things that are truly expensive — a browser, a webdriver session, a seeded database snapshot.
- Global-scope is for things that are
#[ignore]-able by design — integration-test infrastructure you start once (a docker-compose stack, a migrated DB, a webhook listener).