Merge branch 'master' into ab/fix-typing

This commit is contained in:
Alessio Bogon 2024-12-05 22:48:21 +01:00 committed by GitHub
commit 150e07988e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 192 additions and 55 deletions

View File

@ -14,7 +14,29 @@ Added
Changed Changed
+++++++ +++++++
* Step arguments ``"datatable"`` and ``"docstring"`` are now reserved, and they can't be used as step argument names.
Deprecated
++++++++++
Removed
+++++++
Fixed
+++++
Security
++++++++
[8.1.0] - 2024-12-05
----------
Added
+++++
Changed
+++++++
* Step arguments ``"datatable"`` and ``"docstring"`` are now reserved, and they can't be used as step argument names. An error is raised if a step parser uses these names.
* Scenario ``description`` field is now set for Cucumber JSON output.
Deprecated Deprecated
++++++++++ ++++++++++

View File

@ -565,39 +565,6 @@ Example:
assert datatable[1][1] in ["user1", "user2"] assert datatable[1][1] in ["user1", "user2"]
Rules
-----
In Gherkin, `Rules` allow you to group related scenarios or examples under a shared context.
This is useful when you want to define different conditions or behaviours
for multiple examples that follow a similar structure.
You can use either ``Scenario`` or ``Example`` to define individual cases, as they are aliases and function identically.
Additionally, **tags** applied to a rule will be automatically applied to all the **examples or scenarios**
under that rule, making it easier to organize and filter tests during execution.
Example:
.. code-block:: gherkin
Feature: Rules and examples
@feature_tag
Rule: A rule for valid cases
@rule_tag
Example: Valid case 1
Given I have a valid input
When I process the input
Then the result should be successful
Rule: A rule for invalid cases
Example: Invalid case
Given I have an invalid input
When I process the input
Then the result should be an error
Scenario Outlines with Multiple Example Tables Scenario Outlines with Multiple Example Tables
---------------------------------------------- ----------------------------------------------
@ -663,6 +630,91 @@ only the examples under the "Positive results" table will be executed, and the "
pytest -k "positive" pytest -k "positive"
Handling Empty Example Cells
----------------------------
By default, empty cells in the example tables are interpreted as empty strings ("").
However, there may be cases where it is more appropriate to handle them as ``None``.
In such scenarios, you can use a converter with the ``parsers.re`` parser to define a custom behavior for empty values.
For example, the following code demonstrates how to use a custom converter to return ``None`` when an empty cell is encountered:
.. code-block:: gherkin
# content of empty_example_cells.feature
Feature: Handling empty example cells
Scenario Outline: Using converters for empty cells
Given I am starting lunch
Then there are <start> cucumbers
Examples:
| start |
| |
.. code-block:: python
from pytest_bdd import then, parsers
# Define a converter that returns None for empty strings
def empty_to_none(value):
return None if value.strip() == "" else value
@given("I am starting lunch")
def _():
pass
@then(
parsers.re("there are (?P<start>.*?) cucumbers"),
converters={"start": empty_to_none}
)
def _(start):
# Example assertion to demonstrate the conversion
assert start is None
Here, the `start` cell in the example table is empty.
When the ``parsers.re`` parser is combined with the ``empty_to_none`` converter,
the empty cell will be converted to ``None`` and can be handled accordingly in the step definition.
Rules
-----
In Gherkin, `Rules` allow you to group related scenarios or examples under a shared context.
This is useful when you want to define different conditions or behaviours
for multiple examples that follow a similar structure.
You can use either ``Scenario`` or ``Example`` to define individual cases, as they are aliases and function identically.
Additionally, **tags** applied to a rule will be automatically applied to all the **examples or scenarios**
under that rule, making it easier to organize and filter tests during execution.
Example:
.. code-block:: gherkin
Feature: Rules and examples
@feature_tag
Rule: A rule for valid cases
@rule_tag
Example: Valid case 1
Given I have a valid input
When I process the input
Then the result should be successful
Rule: A rule for invalid cases
Example: Invalid case
Given I have an invalid input
When I process the input
Then the result should be an error
Datatables Datatables
---------- ----------

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "pytest-bdd" name = "pytest-bdd"
version = "8.0.0" version = "8.1.0"
description = "BDD for pytest" description = "BDD for pytest"
authors = ["Oleg Pidsadnyi <oleg.pidsadnyi@gmail.com>", "Anatoly Bubenkov <bubenkoff@gmail.com>"] authors = ["Oleg Pidsadnyi <oleg.pidsadnyi@gmail.com>", "Anatoly Bubenkov <bubenkoff@gmail.com>"]
maintainers = ["Alessio Bogon <778703+youtux@users.noreply.github.com>"] maintainers = ["Alessio Bogon <778703+youtux@users.noreply.github.com>"]

View File

@ -189,7 +189,7 @@ class LogBDDCucumberJSON:
"id": test_report_context_registry[report].name, "id": test_report_context_registry[report].name,
"name": scenario["name"], "name": scenario["name"],
"line": scenario["line_number"], "line": scenario["line_number"],
"description": "", "description": scenario["description"],
"tags": self._serialize_tags(scenario), "tags": self._serialize_tags(scenario),
"type": "scenario", "type": "scenario",
"steps": [stepmap(step) for step in scenario["steps"]], "steps": [stepmap(step) for step in scenario["steps"]],

View File

@ -154,6 +154,7 @@ class ScenarioReport:
"name": scenario.name, "name": scenario.name,
"line_number": scenario.line_number, "line_number": scenario.line_number,
"tags": sorted(scenario.tags), "tags": sorted(scenario.tags),
"description": scenario.description,
"feature": { "feature": {
"keyword": feature.keyword, "keyword": feature.keyword,
"name": feature.name, "name": feature.name,

View File

@ -51,9 +51,12 @@ def test_step_trace(pytester):
""" """
@feature-tag @feature-tag
Feature: One passing scenario, one failing scenario Feature: One passing scenario, one failing scenario
This is a feature description
@scenario-passing-tag @scenario-passing-tag
Scenario: Passing Scenario: Passing
This is a scenario description
Given a passing step Given a passing step
And some other passing step And some other passing step
@ -116,72 +119,72 @@ def test_step_trace(pytester):
assert result.ret assert result.ret
expected = [ expected = [
{ {
"description": "", "description": "This is a feature description",
"elements": [ "elements": [
{ {
"description": "", "description": "This is a scenario description",
"id": "test_passing", "id": "test_passing",
"keyword": "Scenario", "keyword": "Scenario",
"line": 5, "line": 6,
"name": "Passing", "name": "Passing",
"steps": [ "steps": [
{ {
"keyword": "Given", "keyword": "Given",
"line": 6, "line": 9,
"match": {"location": ""}, "match": {"location": ""},
"name": "a passing step", "name": "a passing step",
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
}, },
{ {
"keyword": "And", "keyword": "And",
"line": 7, "line": 10,
"match": {"location": ""}, "match": {"location": ""},
"name": "some other passing step", "name": "some other passing step",
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
}, },
], ],
"tags": [{"name": "scenario-passing-tag", "line": 4}], "tags": [{"name": "scenario-passing-tag", "line": 5}],
"type": "scenario", "type": "scenario",
}, },
{ {
"description": "", "description": "",
"id": "test_failing", "id": "test_failing",
"keyword": "Scenario", "keyword": "Scenario",
"line": 10, "line": 13,
"name": "Failing", "name": "Failing",
"steps": [ "steps": [
{ {
"keyword": "Given", "keyword": "Given",
"line": 11, "line": 14,
"match": {"location": ""}, "match": {"location": ""},
"name": "a passing step", "name": "a passing step",
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
}, },
{ {
"keyword": "And", "keyword": "And",
"line": 12, "line": 15,
"match": {"location": ""}, "match": {"location": ""},
"name": "a failing step", "name": "a failing step",
"result": {"error_message": OfType(str), "status": "failed", "duration": OfType(int)}, "result": {"error_message": OfType(str), "status": "failed", "duration": OfType(int)},
}, },
], ],
"tags": [{"name": "scenario-failing-tag", "line": 9}], "tags": [{"name": "scenario-failing-tag", "line": 12}],
"type": "scenario", "type": "scenario",
}, },
{ {
"description": "", "description": "",
"keyword": "Scenario Outline", "keyword": "Scenario Outline",
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}], "tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
"steps": [ "steps": [
{ {
"line": 16, "line": 19,
"match": {"location": ""}, "match": {"location": ""},
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
"keyword": "Given", "keyword": "Given",
"name": "type str and value hello", "name": "type str and value hello",
} }
], ],
"line": 15, "line": 18,
"type": "scenario", "type": "scenario",
"id": "test_passing_outline[str-hello]", "id": "test_passing_outline[str-hello]",
"name": "Passing outline", "name": "Passing outline",
@ -189,17 +192,17 @@ def test_step_trace(pytester):
{ {
"description": "", "description": "",
"keyword": "Scenario Outline", "keyword": "Scenario Outline",
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}], "tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
"steps": [ "steps": [
{ {
"line": 16, "line": 19,
"match": {"location": ""}, "match": {"location": ""},
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
"keyword": "Given", "keyword": "Given",
"name": "type int and value 42", "name": "type int and value 42",
} }
], ],
"line": 15, "line": 18,
"type": "scenario", "type": "scenario",
"id": "test_passing_outline[int-42]", "id": "test_passing_outline[int-42]",
"name": "Passing outline", "name": "Passing outline",
@ -207,17 +210,17 @@ def test_step_trace(pytester):
{ {
"description": "", "description": "",
"keyword": "Scenario Outline", "keyword": "Scenario Outline",
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}], "tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
"steps": [ "steps": [
{ {
"line": 16, "line": 19,
"match": {"location": ""}, "match": {"location": ""},
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
"keyword": "Given", "keyword": "Given",
"name": "type float and value 1.0", "name": "type float and value 1.0",
} }
], ],
"line": 15, "line": 18,
"type": "scenario", "type": "scenario",
"id": "test_passing_outline[float-1.0]", "id": "test_passing_outline[float-1.0]",
"name": "Passing outline", "name": "Passing outline",

View File

@ -320,3 +320,58 @@ def test_forward_slash_in_params(pytester):
result = pytester.runpytest("-s") result = pytester.runpytest("-s")
result.assert_outcomes(passed=1) result.assert_outcomes(passed=1)
assert collect_dumped_objects(result) == ["https://my-site.com"] assert collect_dumped_objects(result) == ["https://my-site.com"]
def test_variable_reuse(pytester):
"""
Test example parameter name and step arg do not redefine each other's value
if the same name is used for both in different steps.
"""
pytester.makefile(
".feature",
outline=textwrap.dedent(
"""\
Feature: Example parameters reuse
Scenario Outline: Check for example parameter re-use
Given the param is initially set from the example table as <param>
When a step arg of the same name is set to "other"
Then the param is still set from the example table as <param>
Examples:
| param |
| value |
"""
),
)
pytester.makepyfile(
textwrap.dedent(
"""\
from pytest_bdd import given, when, then, parsers, scenarios
from pytest_bdd.utils import dump_obj
scenarios('outline.feature')
@given(parsers.parse('the param is initially set from the example table as {param}'))
def _(param):
dump_obj(("param1", param))
@when(parsers.re('a step arg of the same name is set to "(?P<param>.+)"'))
def _(param):
dump_obj(("param2", param))
@then(parsers.parse('the param is still set from the example table as {param}'))
def _(param):
dump_obj(("param3", param))
"""
)
)
result = pytester.runpytest("-s")
result.assert_outcomes(passed=1)
assert collect_dumped_objects(result) == [("param1", "value"), ("param2", "other"), ("param3", "value")]

View File

@ -120,6 +120,7 @@ def test_step_trace(pytester):
"keyword": "Scenario", "keyword": "Scenario",
"line_number": 5, "line_number": 5,
"name": "Passing", "name": "Passing",
"description": "",
"steps": [ "steps": [
{ {
"duration": OfType(float), "duration": OfType(float),
@ -159,6 +160,7 @@ def test_step_trace(pytester):
"keyword": "Scenario", "keyword": "Scenario",
"line_number": 10, "line_number": 10,
"name": "Failing", "name": "Failing",
"description": "",
"steps": [ "steps": [
{ {
"duration": OfType(float), "duration": OfType(float),
@ -197,6 +199,7 @@ def test_step_trace(pytester):
"keyword": "Scenario Outline", "keyword": "Scenario Outline",
"line_number": 14, "line_number": 14,
"name": "Outlined", "name": "Outlined",
"description": "",
"steps": [ "steps": [
{ {
"duration": OfType(float), "duration": OfType(float),
@ -243,6 +246,7 @@ def test_step_trace(pytester):
"keyword": "Scenario Outline", "keyword": "Scenario Outline",
"line_number": 14, "line_number": 14,
"name": "Outlined", "name": "Outlined",
"description": "",
"steps": [ "steps": [
{ {
"duration": OfType(float), "duration": OfType(float),