pythonpython-behave

Python behave fixture on feature level not loaded


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 appended\n"
    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.


Solution

  • 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 appended\n"
        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.