Fixtures

A common task during test execution is to:

  • setup a functionality when a test-scope is entered
  • cleanup (or teardown) the functionality at the end of the test-scope

Fixtures are provided as concept to simplify this setup/cleanup task in behave.

Providing a Fixture

# -- FILE: behave4my_project/fixtures.py  (or in: features/environment.py)
from behave import fixture
from somewhere.browser.firefox import FirefoxBrowser

# -- FIXTURE-VARIANT 1: Use generator-function
@fixture
def browser_firefox(context, timeout=30, **kwargs):
    # -- SETUP-FIXTURE PART:
    context.browser = FirefoxBrowser(timeout, **kwargs)
    yield context.browser
    # -- CLEANUP-FIXTURE PART:
    context.browser.shutdown()
# -- FIXTURE-VARIANT 2: Use normal function
from somewhere.browser.chrome import ChromeBrowser

@fixture
def browser_chrome(context, timeout=30, **kwargs):
    # -- SETUP-FIXTURE PART: And register as context-cleanup task.
    browser = ChromeBrowser(timeout, **kwargs)
    context.browser = browser
    context.add_cleanup(browser.shutdown)
    return browser
    # -- CLEANUP-FIXTURE PART: browser.shutdown()
    # Fixture-cleanup is called when current context-layer is removed.

See also

A fixture is similar to:

Using a Fixture

In many cases, the usage of a fixture is triggered by the fixture-tag in a feature file. The fixture-tag marks that a fixture should be used in this scenario/feature (as test-scope).

# -- FILE: features/use_fixture1.feature
Feature: Use Fixture on Scenario Level

    @fixture.browser.firefox
    Scenario: Use Web Browser Firefox
        Given I load web page "https://somewhere.web"
        ...
    # -- AFTER-SCENARIO: Cleanup fixture.browser.firefox
# -- FILE: features/use_fixture2.feature
@fixture.browser.firefox
Feature: Use Fixture on Feature Level

    Scenario: Use Web Browser Firefox
        Given I load web page "https://somewhere.web"
        ...

    Scenario: Another Browser Test
        ...

# -- AFTER-FEATURE: Cleanup fixture.browser.firefox

A fixture can be used by calling the use_fixture() function. The use_fixture() call performs the SETUP-FIXTURE part and returns the setup result. In addition, it ensures that CLEANUP-FIXTURE part is called later-on when the current context-layer is removed. Therefore, any manual cleanup handling in the after_tag() hook is not necessary.

# -- FILE: features/environment.py
from behave import use_fixture
from behave4my_project.fixtures import browser_firefox

def before_tag(context, tag):
    if tag == "fixture.browser.firefox":
        use_fixture(browser_firefox, context, timeout=10)

Realistic Example

A more realistic example by using a fixture registry is shown below:

# -- FILE: features/environment.py
from behave.fixture import use_fixture_by_tag, fixture_call_params
from behave4my_project.fixtures import browser_firefox, browser_chrome

# -- REGISTRY DATA SCHEMA 1: fixture_func
fixture_registry1 = {
    "fixture.browser.firefox": browser_firefox,
    "fixture.browser.chrome":  browser_chrome,
}
# -- REGISTRY DATA SCHEMA 2: (fixture_func, fixture_args, fixture_kwargs)
fixture_registry2 = {
    "fixture.browser.firefox": fixture_call_params(browser_firefox),
    "fixture.browser.chrome":  fixture_call_params(browser_chrome, timeout=12),
}

def before_tag(context, tag):
    if tag.startswith("fixture."):
        return use_fixture_by_tag(tag, context, fixture_registry1):
    # -- MORE: Tag processing steps ...
# -- FILE: behave/fixture.py
# ...
def use_fixture_by_tag(tag, context, fixture_registry):
    fixture_data = fixture_registry.get(tag, None)
    if fixture_data is None:
        raise LookupError("Unknown fixture-tag: %s" % tag)

    # -- FOR DATA SCHEMA 1:
    fixture_func = fixture_data
    return use_fixture(fixture_func, context)

    # -- FOR DATA SCHEMA 2:
    fixture_func, fixture_args, fixture_kwargs = fixture_data
    return use_fixture(fixture_func, context, *fixture_args, **fixture_kwargs)

Hint

Naming Convention for Fixture Tags

Fixture tags should start with "@fixture.*" prefix to improve readability and understandibilty in feature files (Gherkin).

Tags are used for different purposes. Therefore, it should be clear when a fixture-tag is used.

Fixture Cleanup Points

The point when a fixture-cleanup is performed depends on the scope where use_fixture() is called (and the fixture-setup is performed).

Context Layer Fixture-Setup Point Fixture-Cleanup Point
test run In before_all() hook After after_all() at end of test-run.
feature In before_feature() After after_feature(), at end of feature.
feature In before_tag() After after_feature() for feature tag.
scenario In before_scenario() After after_scenario(), at end of scenario.
scenario In before_tag() After after_scenario() for scenario tag.
scenario In a step After after_scenario(). Fixture is usable until end of scenario.

Fixture Setup/Cleanup Semantics

If an error occurs during fixture-setup (meaning an exception is raised):

  • Feature/scenario execution is aborted
  • Any remaining fixture-setups are skipped
  • After feature/scenario hooks are processed
  • All fixture-cleanups and context cleanups are performed
  • The feature/scenario is marked as failed

If an error occurs during fixture-cleanup (meaning an exception is raised):

  • All remaining fixture-cleanups and context cleanups are performed
  • First cleanup-error is reraised to pass failure to user (test runner)
  • The feature/scenario is marked as failed

Ensure Fixture Cleanups with Fixture Setup Errors

Fixture-setup errors are special because a cleanup of a fixture is in many cases not necessary (or rather difficult because the fixture object is only partly created, etc.). Therefore, if an error occurs during fixture-setup (meaning: an exception is raised), the fixture-cleanup part is normally not called.

If you need to ensure that the fixture-cleanup is performed, you need to provide a slightly different fixture implementation:

# -- FILE: behave4my_project/fixtures.py  (or: features/environment.py)
from behave import fixture
from somewhere.browser.firefox import FirefoxBrowser

def setup_fixture_part2_with_error(arg):
    raise RuntimeError("OOPS-FIXTURE-SETUP-ERROR-HERE)

# -- FIXTURE-VARIANT 1: Use generator-function with try/finally.
@fixture
def browser_firefox(context, timeout=30, **kwargs):
    try:
        browser = FirefoxBrowser(timeout, **kwargs)
        browser.part2 = setup_fixture_part2_with_error("OOPS")
        context.browser = browser   # NOT_REACHED
        yield browser
        # -- NORMAL FIXTURE-CLEANUP PART: NOT_REACHED due to setup-error.
     finally:
        browser.shutdown()  # -- CLEANUP: When generator-function is left.
# -- FIXTURE-VARIANT 2: Use normal function and register cleanup-task early.
from somewhere.browser.chrome import ChromeBrowser

@fixture
def browser_chrome(context, timeout=30, **kwargs):
    browser = ChromeBrowser(timeout, **kwargs)
    context.browser = browser
    context.add_cleanup(browser.shutdown)   # -- ENSURE-CLEANUP EARLY
    browser.part2 = setup_fixture_part2_with_error("OOPS")
    return browser  # NOT_REACHED
    # -- CLEANUP: browser.shutdown() when context-layer is removed.

Note

An fixture-setup-error that occurs when the browser object is created, is not covered by these solutions and not so easy to solve.

Composite Fixtures

The last section already describes some problems when you use complex or composite fixtures. It must be ensured that cleanup of already created fixture parts is performed even when errors occur late in the creation of a composite fixture. This is basically a scope guard problem.

Solution 1:

# -- FILE: behave4my_project/fixtures.py
# SOLUTION 1: Use "use_fixture()" to ensure cleanup even in case of errors.
from behave import fixture, use_fixture

@fixture
def foo(context, *args, **kwargs):
    pass    # -- FIXTURE IMPLEMENTATION: Not of interest here.

@fixture
def bar(context, *args, **kwargs):
    pass    # -- FIXTURE IMPLEMENTATION: Not of interest here.

# -- SOLUTION: With use_fixture()
# ENSURES: foo-fixture is cleaned up even when setup-error occurs later.
@fixture
def composite1(context, *args, **kwargs):
    the_fixture1 = use_fixture(foo, context)
    the_fixture2 = use_fixture(bar, context)
    return [the_fixture1, the_fixture2]

Solution 2:

# -- ALTERNATIVE SOLUTION: With use_composite_fixture_with()
from behave import fixture
from behave.fixture import use_composite_fixture_with, fixture_call_params

@fixture
def composite2(context, *args, **kwargs):
    the_composite = use_composite_fixture_with(context, [
        fixture_call_params(foo, name="foo"),
        fixture_call_params(bar, name="bar"),
    ])
    return the_composite