Hooks
View SourceCucumber for Elixir provides hooks that allow you to run code around the whole run (before_all/after_all), around each scenario (before_scenario/after_scenario), and around each step (before_step/after_step). This is useful for setup and teardown operations like database transactions, authentication, or any other cross-cutting concerns.
Overview
Hooks are defined in support files placed in test/features/support/ and are automatically discovered and executed at the appropriate times during test execution.
Defining Hooks
To define hooks, create a module that uses Cucumber.Hooks:
# test/features/support/database_support.exs
defmodule DatabaseSupport do
use Cucumber.Hooks
# Global hook - runs before every scenario
before_scenario context do
# Your setup code here
{:ok, Map.put(context, :setup_done, true)}
end
# Tagged hook - only runs for scenarios with @database tag
before_scenario "@database", context do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
if context.async do
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
end
{:ok, context}
end
# After hooks run in reverse order of definition
after_scenario _context do
# Cleanup code
:ok
end
endHook Types
Before Scenario Hooks
Run before each scenario:
# Global before hook
before_scenario context do
# Runs before every scenario
{:ok, context}
end
# Tagged before hook
before_scenario "@slow", context do
# Only runs for scenarios tagged with @slow
{:ok, Map.put(context, :timeout, 30_000)}
endAfter Scenario Hooks
Run after each scenario:
# Global after hook
after_scenario context do
# Runs after every scenario
:ok
end
# Tagged after hook
after_scenario "@api", context do
# Only runs for scenarios tagged with @api
# Clean up API state
:ok
endRun-Level Hooks (BeforeAll/AfterAll)
before_all hooks run lazily, exactly once per test run, before the first
scenario that executes — serialized through a run coordinator, so they are
safe with @async features. Their accumulated context map is merged into
every scenario's context. If a before_all hook fails, the remaining
before_all hooks still run (to set up as much as possible for cleanup),
but every scenario in the run fails with the original error.
after_all hooks run once after the whole suite, in reverse definition
order, receiving the before_all context with the ExUnit suite summary
merged under :suite_result. A failing after_all hook fails the run; the
remaining after_all hooks still run.
Run-level hooks cannot be tagged (they don't belong to any scenario):
before_all context do
{:ok, server} = start_external_service()
{:ok, Map.put(context, :service, server)}
end
after_all context do
stop_external_service(context.service)
:ok
endStep Hooks
before_step and after_step hooks bracket every step of matching
scenarios, including background steps. They receive the step's prepared
context — :step (the Gherkin.Step struct), :args, and any
:datatable/:docstring. after_step additionally sees :step_status
(:passed, :failed, :pending, or :skipped) and runs for failing
steps too.
A {:error, reason} from a before_step hook fails the scenario without
running the step body; :skipped/:pending signals from before_step
halt the scenario just like the step itself returning them. after_step
return values are ignored.
before_step "@traced", context do
IO.puts("→ #{context.step.keyword} #{context.step.text}")
:ok
end
after_step "@traced", context do
IO.puts("← #{context.step.text}: #{context.step_status}")
:ok
endNamed Hooks
Any hook can be given a name:, which appears in failure output. Names
also lift the one-hook-per-kind restriction, so a module can define any
number of distinctly-named hooks of the same kind:
before_scenario context, name: "prepare database" do
{:ok, context}
end
before_scenario "@admin", context, name: "sign in as admin" do
{:ok, context}
endReturn Values
Hooks support the same return values as step definitions:
:ok- Keeps the context unchanged{:ok, map}- Merges the map into the context%{} = map- Merges the map into the context{:error, reason}- Fails the scenario before it starts (steps and after hooks don't run):skippedor{:skipped, reason}(before hooks only) - Skips the whole scenario without failing it: remaining before hooks and all steps are skipped, after hooks still run:pendingor{:pending, message}(before hooks only) - Like:skipped, but the scenario fails withCucumber.PendingStepError
Return values from after hooks are ignored, so an after hook returning
:skipped only marks itself — subsequent after hooks still run.
Hook Execution Order
before_allhooks run once, in definition order, before the first scenario- Before hooks run in the order they are defined
before_step/after_stephooks bracket each step (after-step in reverse order)- After hooks run in reverse order (last defined runs first)
after_allhooks run once, in reverse order, after the whole suite- Tagged hooks only run for scenarios with matching tags
- Global hooks run for all scenarios
Tag Inheritance
Feature-level tags are inherited by all scenarios in that feature:
@database
Feature: User Management
# All scenarios inherit @database tag
Scenario: Create user
# This scenario has @database tag
Given a new user
@api
Scenario: API user creation
# This scenario has both @database and @api tags
Given an API requestContext Variables
The context passed to hooks includes:
:scenario_name- The name of the current scenario:async- Whether the feature is running in async mode:step_history- List of steps executed (empty in before hooks)- Any data added by previous hooks or steps
Practical Examples
Database Setup
defmodule DatabaseSupport do
use Cucumber.Hooks
before_scenario "@database", context do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
if context.async do
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
end
{:ok, context}
end
endAuthentication
defmodule AuthSupport do
use Cucumber.Hooks
before_scenario "@authenticated", context do
user = MyApp.Factory.insert(:user)
token = MyApp.Auth.generate_token(user)
{:ok, Map.merge(context, %{
current_user: user,
auth_token: token
})}
end
endPerformance Monitoring
defmodule PerformanceSupport do
use Cucumber.Hooks
before_scenario "@performance", context do
start_time = System.monotonic_time()
{:ok, Map.put(context, :start_time, start_time)}
end
after_scenario "@performance", context do
duration = System.monotonic_time() - context.start_time
milliseconds = System.convert_time_unit(duration, :native, :millisecond)
IO.puts("Scenario completed in #{milliseconds}ms")
:ok
end
endConfiguration
By default, support files are loaded from test/features/support/**/*.exs. You can customize this in your config:
# config/test.exs
config :cucumber,
support: ["test/support/**/*.exs", "test/cucumber_support/**/*.exs"]Best Practices
- Keep hooks focused - Each hook should have a single responsibility
- Use tags wisely - Don't create too many specialized hooks
- Avoid side effects - Hooks should be predictable and repeatable
- Clean up in after hooks - Ensure proper cleanup even if scenarios fail
- Use context passing - Share data between hooks and steps via context
Troubleshooting
Hooks not running
- Ensure your support files are in the correct directory
- Verify the module uses
Cucumber.Hooks - Check that tags match exactly (including the @ symbol)
- Confirm the file has a
.exsextension
Hook execution order
Remember that:
- Before hooks run in definition order
- After hooks run in reverse definition order
- Tagged hooks only run when tags match