# 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:

- a `contextlib.contextmanager()`

- a pytest.fixture

- the scope guard idiom

## 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