Hooks

Hooks let you run setup and teardown code around examples. Behave provides six hooks:

Hook Fires
before-all Once, before the first example in the group
before-each Before every example in the group (and inherited by nested groups)
after-each After every example in the group (and inherited by nested groups)
after-all Once, after the last example in the group
around-each Wraps each example and its before-each / after-each hooks
around-all Wraps the entire group, including before-all / after-all

All six are exported from BDD::Behave and must be called inside a describe or context block.

Basic example#

describe 'a counter', {
  my $count;

  before-each {
    $count = 0;
  }

  it 'starts at zero', {
    expect($count).to.be(0);
  }

  it 'is reset between examples', {
    $count++;
    expect($count).to.be(1);
  }
}

Ordering and inheritance#

Within a group, hooks run in registration order. When examples live in nested groups, outer before-each hooks fire outer-to-inner, and outer after-each hooks fire inner-to-outer:

describe 'outer', {
  before-each { trace('outer-before') }
  after-each  { trace('outer-after')  }

  context 'inner', {
    before-each { trace('inner-before') }
    after-each  { trace('inner-after')  }

    it 'runs', {
      # trace so far: outer-before, inner-before
      trace('example');
    }
    # after the example: inner-after, outer-after
  }
}

before-all / after-all#

These fire exactly once per group, useful for expensive shared setup.

describe 'database queries', {
  before-all {
    setup-fixture-data;
  }

  after-all {
    teardown-fixture-data;
  }

  it 'finds a user', { ... }
  it 'finds a page', { ... }
}

Warning

Anything before-all mutates is shared across the group's examples. Prefer before-each for per-example isolation.

around-each and around-all#

Around hooks receive a continuation: a Callable that runs everything inside the wrapper. The hook is responsible for invoking it. This makes around hooks the right tool for paired setup/teardown that must bracket the example, even across exceptions.

around-each wraps each example, including its before-each and after-each hooks:

describe 'users repository', {
  around-each -> &continue {
    begin-transaction;
    LEAVE rollback-transaction;
    continue();
  }

  it 'creates a user', { ... }
  it 'finds a user',   { ... }
}

around-all wraps the entire group, including before-all and after-all:

describe 'expensive integration suite', {
  around-all -> &continue {
    boot-fixture-server;
    LEAVE shutdown-fixture-server;
    continue();
  }

  before-all { ... }   # runs *inside* the around-all wrapper
  it 'a',     { ... }
  it 'b',     { ... }
}

Either form of around hook can also be written as a bare block that uses the implicit $_:

around-each {
  setup-context;
  $_();
  teardown-context;
}

Composition order#

When a single group registers multiple around hooks, the first-registered hook is outermost: the second is wrapped by the first, and so on. Across nested groups, outer-group around hooks wrap inner-group around hooks:

describe 'outer', {
  around-each -> &c { trace('outer-start'); c(); trace('outer-end') }

  context 'inner', {
    around-each -> &c { trace('inner-start'); c(); trace('inner-end') }

    it 'runs', { trace('body') }
    # trace: outer-start, inner-start, body, inner-end, outer-end
  }
}

Skipping the continuation#

If an around hook returns without invoking the continuation, the example (or group) is reported as skipped and counted in the suite's skipped total. This lets you write conditional skips inline:

around-each -> &continue {
  unless $env-supports-feature {
    return;   # example is recorded as skipped
  }
  continue();
}

Exceptions#

If an around hook throws before invoking the continuation, the example is recorded as a failure and the exception is captured. If a hook throws after the continuation completes, Behave prints a warning but does not overwrite the example's already-recorded result.

Exceptions raised inside the example body are still handled by Behave's existing expect / Failures machinery and do not propagate up through the continuation. Wrap the example body with the relevant matcher instead.

Metadata filters#

Around hooks accept the same metadata filters as the other hook phases (see below):

around-each :tag<db>, -> &continue {
  begin-transaction;
  LEAVE rollback-transaction;
  continue();
}

For around-all, the hook fires only when at least one descendant example matches the filter, mirroring before-all / after-all filter semantics.

Metadata-keyed hooks#

All six hooks accept metadata filters that gate when the hook fires. Filters are passed as named arguments before the block:

before-each :tag<database>, {
  setup-fixture;
}

after-each :tag<database>, {
  rollback-fixture;
}

The hook only fires for examples whose effective tags or metadata match the filter. Effective metadata is collected by walking the ancestry from the example up through every enclosing describe/context, so a tag on the outer group is inherited by every example below it.

Tag filters#

Filter Meaning
:tag<name> example must have this tag
:tags<a b c> example must have all of these tags (AND)
:exclude-tag<name> example must not have this tag
:exclude-tags<a b> example must not have any of these tags

Arbitrary metadata#

Any other named argument on it / describe / context becomes example metadata, and any other named argument on a hook becomes a metadata filter:

describe 'user-service', :type<model>, {
  before-each :type<model>, {
    seed-models;
  }

  it 'finds a user', { ... }   # type<model> inherited; hook fires
}

context 'view layer', :type<view>, {
  before-each :type<view>, {
    setup-renderer;
  }

  it 'renders', { ... }
}

A filter value that is a list (e.g. :role<admin staff>) matches when the example's stored metadata contains any of the listed values.

AND semantics across keys#

When a hook lists multiple filter keys, every key must match:

# Fires only when an example is tagged :db AND not tagged :read-only
before-each :tag<db>, :exclude-tag<read-only>, {
  begin-transaction;
}

To get OR-style behavior, register multiple hooks.

before-all and after-all filters#

Filters on before-all / after-all fire once per group, but only when at least one descendant example matches. If a tag-filtered before-all has no matching descendants (for example because all matching examples were excluded by the runner's --tag filter), the hook is skipped entirely along with its paired after-all.

describe 'database tests', {
  before-all :tag<expensive>, {
    seed-large-fixture;   # only when an :expensive example will run
  }

  after-all :tag<expensive>, {
    drop-large-fixture;
  }

  it 'cheap',     :tag<cheap>,     { ... }   # hook does not fire
  it 'expensive', :tag<expensive>, { ... }   # hook fires once for the group
}