Python behave fixture on feature level not loaded

Question:

This is somewhat related to this question, but I have some further problems in this minimal example below.

For a feature test I prepared a fixture which backs up a file which shall be modified during the test run (e.g. a line is appended). After the test run this fixture restores the original file.

Project Files:

└───features
    │   environment.py
    │   modify_file.feature
    │
    └───steps
            file_ops.py
#!/usr/bin/env python
# FILE: features/environment.py

import logging
from behave import fixture
from behave.runner import Context

logger = logging.getLogger(__name__)


@fixture
def backup_file(context: Context):
    """
    A file will be modified during the feature test.
    This fixture shall backup the file before the feature test
    and restore the backup after the test.
    """
    file = Path.home() / "important.txt"
    backup_suffix = ".backup"
    file.touch()
    file.replace(file.with_suffix(backup_suffix))
    logger.info("File backed up")
    yield
    file.with_suffix(backup_suffix).replace(file)
    logger.info("File restored")
# FILE: features/modify_file.feature

@fixture.backup.file
Feature: Modify file

        @wip
        Scenario: Append a line to a file
                Given the file exists
                When I append a line to the file
                Then the line appears at the end of the file
#!/usr/bin/env python
# File features/steps/file_ops.py

from pathlib import Path
from behave import given
from behave import when
from behave import then
from behave.runner import Context
import logging

logger = logging.getLogger(__name__)
file = Path.home() / "important.txt"


@given("the file exists")
def step_impl(context: Context):
    logger.info(f"Touching file")
    file.touch()


@when("I append a line to the file")
def step_impl(context: Context):
    logger.info(f"Appending a line to file")
    context.appended = "Test line appendedn"
    with open(file, mode="a") as f:
        f.write(context.appended)


@then("the line appears at the end of the file")
def step_impl(context: Context):
    logger.info(f"Checking if line was appended")
    with open(file, mode="r") as f:
        for line in f:
            pass
        logger.info(f"Last line is '{line.strip()}'")
        assert line == context.appended

I want to apply the fixture at the feature level before all scenarios are run. The file shall be restored after all scenarios have run. However, this is apparently not the case.

When I run behave -w (no log capture, wip tags only), I don’t see any log lines from the fixture being output and also with every run I see another line appended to the file. This means the file is not being backed up and restored. The fixture is not applied even if in the I move the fixture down to the Scenario level modify_file.feature file.

Can you help me understand what is going on here? I’m also curious why the fixture tag is used with dot notation (@fixture.backup.file) rather than (fixture.backup_file) as this would be similar to the actual function name. There is no explanation of this in the behave documentation.

Asked By: Zababa

||

Answers:

Here is how you can do it without @fixture, just by using the before_tag hook:

#!/usr/bin/env python
# FILE: features/environment.py

import logging
from behave.runner import Context
from behave.model import Tag
from pathlib import Path

logger = logging.getLogger(__name__)


def rename_file(origin: Path, target: Path) -> tuple[Path, Path]:
    """
    Will rename the `origin` to `target`, replaces existing `target`
    """
    origin.touch()
    backup = origin.replace(target)
    logger.info(f"File {str(origin)} renamed to {str(target)}")
    return origin, backup


def before_tag(context: Context, tag: Tag):
    if tag == "backup.file.important.txt":
        file = Path.home() / "important.txt"
        backup = file.with_suffix(".backup")
        context.file, context.backup_file = rename_file(file, backup)
        context.add_cleanup(rename_file, context.backup_file, context.file)
#!/usr/bin/env python
# File features/steps/file_ops.py

from pathlib import Path
from behave import given
from behave import when
from behave import then
from behave.runner import Context
import logging

logger = logging.getLogger(__name__)


@given("the file exists")
def step_impl(context: Context):
    logger.info(f"Touching file")
    context.file.touch()


@when("I append a line to the file")
def step_impl(context: Context):
    logger.info(f"Appending a line to file")
    context.appended = "Test line appendedn"
    with open(context.file, mode="a") as f:
        f.write(context.appended)


@then("the line appears at the end of the file")
def step_impl(context: Context):
    logger.info(f"Checking if line was appended")
    with open(context.file, mode="r") as f:
        for n, line in enumerate(f, start=1):
            logger.info(f"Line {n} is {line}")
        logger.info(f"Last line is '{line.strip()}'")
        assert line == context.appended

But using a fixture and the fixture registry as described in the Realistic example in behave’s documentation is superior, as you will likely end up having many more setup-cleanup pairs later on.

Answered By: Zababa

I also had trouble setting up a fixture because the docs aren’t super clear that you have to explicitly enable them. It isn’t enough to have the @fixture decoration in features/environment.py. You also have to call use_fixture(). For example, inside before_tag(), like this:

def before_tag(context, tag):
    if tag == "fixture.backup.file":
        use_fixture(backup_file, context)

This example helped me figure that out:
https://github.com/behave/behave/blob/main/features/fixture.feature#L16

Answered By: remcycles
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.