Skip to content

feat: Allure API implementation for Pytest-BDD #845

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 26, 2025
Merged

Conversation

delatrie
Copy link
Contributor

@delatrie delatrie commented Mar 24, 2025

Context

The PR implements the missing parts of Allure API in Pytest-BDD. Check #726 for the complete list of the implemented API pieces.

API usage notes

To use Allure API, apply a decorator or call a runtime function (allure.dynamic.*, allure.attach, or allure.attach.file).

Allure decorators

General usage: apply decorators to a scenario test function:

@allure.id("1001")
@allure.epic("My Epic")
@allure.issue("https://github.com/allure-framework/allure-python/issues/726", name="Issue 726")
@scenario("my_feature.feature", "My Scenario")
def test_scenario():
    pass

Warning

It is important to put Allure decorators above @scenario. In other words, the following won't work:

@scenario("my_feature.feature", "My Scenario")
@allure.epic("My Epic") # This decorator will be lost
def test_scenario():
    pass

Apply to whole modules

All Allure decorators except @allure.title and @allure.step are also proper pytest markers, which means they can be applied to the whole module via the pytestmark global variable.

pytestmark = [
    allure.epic("My Epic"),
    allure.link("https://allurereport.org", name="Homepage"),
]

@scenario("my_feature.feature", "My Scenario 1")
def test_scenario_1(): # Gets the epic and the link
    pass

@scenario("my_feature.feature", "My Scenario 1")
def test_scenario_2(): # Gets the epic and the link as well
    pass

Note

Although pytest markers can also be defined at the class level, Pytest-BDD doesn't support defining scenario test functions inside a class (see pytest-dev/pytest-bdd#325).

The scenarios shortcut

Allure decorators can't be applied directly to test functions defined with the scenarios shortcut. But you have some options to tackle that.

Option 1 is to define separate test functions with the @scenario decorator and apply Allure decorators to them. Make sure all such functions are defined before scenarios is called:

# defines a test function for a specific scenario from the feature file and assigns an Allure ID to the function
@allure.id("1001")
@scenario("my_feature.feature", "A scenario with ID")
def test_scenario_with_id():
    pass

# defines test functions for all other scenarios from the feature file
scenarios("my_feature.feature")

This works well for scenario-specific information like IDs, titles, descriptions, issue links, etc.

Option 2 is to use pytestmark as in one of the above examples:

pytestmark = [
    allure.epic("My Epic"),
    allure.link("https://allurereport.org", name="Homepage"),
]

# Defines test functions for all scenarios from the feature file. The functions are affected by all of the above markers.
scenarios("my_feature.feature")

This is preferable for information that applies to many scenarios, like epics, general links, suites, tags, etc.

Option 3: call the runtime counterparts from a step function:

scenarios("my_feature.feature")

@given("my step")
def _():
    allure.dynamic.link("https://allurereport.org/docs/pytestbdd/", name="Docs")

Note

When possible, prefer Allure decorators over runtime functions. The decorators will affect the test results regardless of the outcome. On the other hand, the runtime function calls may be skipped because of failures in fixtures or preceding steps.

About @allure.title

When applied to a scenario function, @allure.title supports interpolation of outline arguments and pytest parameters:

Feature: My feature
  Scenario Outline: My scenario outline
    Given step 1

    Examples:
      | outline_arg |
      | gherkin     |
@allure.title("Outline arg: {outline_arg}, pytest arg: {pytest_arg}")
@pytest.mark.parametrize("pytest_arg", ["pytest"])
@scenario("my_feature.feature", "My scenario outline")
def test_my_scenario(pytest_arg):
    pass

The test result will be displayed as Outline arg: gherkin, pytest arg: pytest. You can read more about the syntax of replacement fields here.

Note

Fixture values currently can't be interpolated into scenario names.

When applied to a step implementation, @allure.title will update the step's name. The following stuff can be interpolated:

  • The function's arguments including fixtures (e.g., target fixtures of the previous steps).
  • Pytest parameters.
  • Scenario outline parameters

Example:

Feature: My feature
  Scenario Outline: My scenario outline
    Given step 1
    Then step 2

    Examples:
      | outline_arg |
      | gherkin     |
@pytest.fixture
def fixture_1():
    yield "fixture"

@pytest.mark.parametrize("pytest_arg", ["pytest"])
@scenario("my_feature.feature", "My scenario outline")
def test_my_scenario(pytest_arg):
    pass

@given("step 1", target_fixture="target_fixture_1")
def _():
    return "step-result"

@then(parsers.parse("step {step_arg}"))
@allure.title("Outline arg: {outline_arg}, pytest arg: {pytest_arg}, fixture value: {fixture_1}, prev step result: {target_fixture_1}, step arg: {step_arg}")
def _(fixture_1, target_fixture_1, step_arg):
    pass

Here, the title will be Outline arg: gherkin, pytest arg: pytest, fixture value: fixture, prev step result: step-result, step arg: 2

This implements #737.

Runtime functions

The Allure Runtime API functions can be called anytime between pytest_bdd_before_scenario and the last call to pytest_runtest_makereport for the current item. That includes step implementations, scenario test functions, and some pytest hooks:

@scenario("my_feature.feature", "My scenario")
def test_my_scenario():
    allure.dynamic.epic("My epic")

@given("my step")
def _():
    allure.attach("My attachment data", name="My attachment", attachment_type=allure.attachment_type.TEXT)

In conftest.py:

def pytest_runtest_teardown(item):
    allure.attach.file("screenshot.png", name="Screenshot", attachment_type=allure.attachment_type.PNG)

allure.dynamic.title

Unlike its decorator counterpart, allure.dynamic.title always changes the test result's name. It never affects a step's name.
As in allure-pytest, no interpolation is supported: the caller must fully construct a value.

@given("my step")
def _():
    allure.dynamic.title("A new name") # updates the test's name; doesn't change the step's name

allure.attach and allure.attach.file

These functions add attachments to the innermost scope. If a step is running, the attachment will go to the current step. If no step is running, the attachment will be added to the test result.

allure.dynamic.parameter

This function always adds a new parameter to the test result. It neither changes the value or metadata of an existing parameter nor adds parameters to steps.

The mode and excluded arguments work just like in pytest.

allure.step

Substeps can now be added with allure.step. It works just like in pytest.

Other changes

Data tables and doc strings

Pytest-BDD 8.0 features data tables and doc strings. This PR supports both arguments by converting them to the corresponding attachments. This implements #844.

This also works for older versions of Pytest-BDD. In the case of data tables, you should name the argument datatable and define a type converter. The converter must return the data table as list[list[str]]:

def parse_data_table(text) -> list[list[str]]:
    return [
        [x.strip() for x in line.split("|")]
        for line in (x.strip("|") for x in text.splitlines())
    ]

@given(parsers.parse("a step with a data table\\n{datatable:Datatable}", extra_types={"Datatable": parse_data_table}))
def _(datatable):
    pass

For doc strings, you should only name the argument docstring. The type converter (if used) must return str:

@given(parsers.parse('a step with a doc string\\n"""{docstring}"""'))
def _(docstring):
    pass

Feature and scenario descriptions

Feature and scenario descriptions from feature files are now shown as test result descriptions (separated by an empty line).

@allure.description and allure.dynamic.description overwrite these descriptions.

Step arguments

Arguments of a step implementation function are now shown as the step's parameters in the report.

Xfail support

Allure Pytest-BDD now correctly interprets expected failures:

  • A scenario with an expected failure is reported as skipped (this includes satisfied @pytest.mark.xfail markers and calls to pytest.xfail).
  • A scenario with an unsatisfied expected failure (XPASS) is reported as passed with the status details.
  • A scenario with an unsatisfied strict expected failure (XPASS(strict)) is reported as broken.

Broken status support

Scenarios/steps with unexpected errors are now reported as broken (with yellow). Scenarios/steps with failed assertions are still reported as failed (with red).

Link templates

Link templates are now supported via the --allure-link-pattern CLI option. The format is the same as for allure-pytest. You may put this option in your pytest configuration to automatically add it on each run. For example, in pyproject.toml:

[tool.pytest.ini_options]
addopts = [
    "--alluredir", "allure-results",
    "--allure-link-pattern", "issue:https://github.com/allure-framework/allure-python/issues/{}"
]

Note

If a value passed to an Allure link API is already a proper URL, no template is used. The value will be reported as is.

Pytest and gherkin tags

Argument-less pytest markers (except built-in markers) are now converted to Allure tags. Since Pytest-BDD implements gherkin tags as custom pytest markers, those are converted to tags as well.

Given the following feature file:

@foo
Feature: My feature
  @bar
  Scenario: My scenario
    Given my step

And the following Python file:

@pytest.mark.baz
@scenario("my_feature.feature", "My scenario")
def test_my_scenario():
    pass

The test result will contain three tags: foo, bar, and baz.

Minor changes and fixes

  • Pytest parameter values now are not included in the default test result name (they are already visible next to the name)
  • Skipped teardowns now don't overwrite original statuses of scenarios (including passed)
  • Classifiers for packages are fixed to reflect the currently supported Python versions (from 3.8 to 3.13)
  • Add the Topic :: Software Development :: Testing :: BDD classifier to allure-pytest-bdd.
  • Fix pytest parameter serialization (Allure report showing NaN% while using --device matrix capability in Json format #655).
  • Test coverage is improved

Fixes #655
Closes #726
Closes #737
Closes #844

Checklist

@delatrie delatrie changed the title Pytest bdd allure api feat: Allure API implementation for Pytest-BDD Mar 24, 2025
@delatrie delatrie marked this pull request as ready for review March 24, 2025 18:17
@baev baev merged commit 1ceb156 into master Mar 26, 2025
54 checks passed
@baev baev deleted the pytest-bdd-allure-api branch March 26, 2025 13:01
@delatrie delatrie added type:new feature Pull requests that introduce new features and removed type:enhancement labels Apr 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
theme:pytest-bdd type:new feature Pull requests that introduce new features
Projects
None yet
2 participants