Merge branch 'master' into ab/fix-typing
This commit is contained in:
commit
150e07988e
24
CHANGES.rst
24
CHANGES.rst
|
@ -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
|
||||||
++++++++++
|
++++++++++
|
||||||
|
|
118
README.rst
118
README.rst
|
@ -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
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
|
|
@ -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>"]
|
||||||
|
|
|
@ -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"]],
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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")]
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in New Issue