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
|
||||
+++++++
|
||||
* 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
|
||||
++++++++++
|
||||
|
|
118
README.rst
118
README.rst
|
@ -565,39 +565,6 @@ Example:
|
|||
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
|
||||
----------------------------------------------
|
||||
|
||||
|
@ -663,6 +630,91 @@ only the examples under the "Positive results" table will be executed, and the "
|
|||
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
|
||||
----------
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "pytest-bdd"
|
||||
version = "8.0.0"
|
||||
version = "8.1.0"
|
||||
description = "BDD for pytest"
|
||||
authors = ["Oleg Pidsadnyi <oleg.pidsadnyi@gmail.com>", "Anatoly Bubenkov <bubenkoff@gmail.com>"]
|
||||
maintainers = ["Alessio Bogon <778703+youtux@users.noreply.github.com>"]
|
||||
|
|
|
@ -189,7 +189,7 @@ class LogBDDCucumberJSON:
|
|||
"id": test_report_context_registry[report].name,
|
||||
"name": scenario["name"],
|
||||
"line": scenario["line_number"],
|
||||
"description": "",
|
||||
"description": scenario["description"],
|
||||
"tags": self._serialize_tags(scenario),
|
||||
"type": "scenario",
|
||||
"steps": [stepmap(step) for step in scenario["steps"]],
|
||||
|
|
|
@ -154,6 +154,7 @@ class ScenarioReport:
|
|||
"name": scenario.name,
|
||||
"line_number": scenario.line_number,
|
||||
"tags": sorted(scenario.tags),
|
||||
"description": scenario.description,
|
||||
"feature": {
|
||||
"keyword": feature.keyword,
|
||||
"name": feature.name,
|
||||
|
|
|
@ -51,9 +51,12 @@ def test_step_trace(pytester):
|
|||
"""
|
||||
@feature-tag
|
||||
Feature: One passing scenario, one failing scenario
|
||||
This is a feature description
|
||||
|
||||
@scenario-passing-tag
|
||||
Scenario: Passing
|
||||
This is a scenario description
|
||||
|
||||
Given a passing step
|
||||
And some other passing step
|
||||
|
||||
|
@ -116,72 +119,72 @@ def test_step_trace(pytester):
|
|||
assert result.ret
|
||||
expected = [
|
||||
{
|
||||
"description": "",
|
||||
"description": "This is a feature description",
|
||||
"elements": [
|
||||
{
|
||||
"description": "",
|
||||
"description": "This is a scenario description",
|
||||
"id": "test_passing",
|
||||
"keyword": "Scenario",
|
||||
"line": 5,
|
||||
"line": 6,
|
||||
"name": "Passing",
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Given",
|
||||
"line": 6,
|
||||
"line": 9,
|
||||
"match": {"location": ""},
|
||||
"name": "a passing step",
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
},
|
||||
{
|
||||
"keyword": "And",
|
||||
"line": 7,
|
||||
"line": 10,
|
||||
"match": {"location": ""},
|
||||
"name": "some other passing step",
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
},
|
||||
],
|
||||
"tags": [{"name": "scenario-passing-tag", "line": 4}],
|
||||
"tags": [{"name": "scenario-passing-tag", "line": 5}],
|
||||
"type": "scenario",
|
||||
},
|
||||
{
|
||||
"description": "",
|
||||
"id": "test_failing",
|
||||
"keyword": "Scenario",
|
||||
"line": 10,
|
||||
"line": 13,
|
||||
"name": "Failing",
|
||||
"steps": [
|
||||
{
|
||||
"keyword": "Given",
|
||||
"line": 11,
|
||||
"line": 14,
|
||||
"match": {"location": ""},
|
||||
"name": "a passing step",
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
},
|
||||
{
|
||||
"keyword": "And",
|
||||
"line": 12,
|
||||
"line": 15,
|
||||
"match": {"location": ""},
|
||||
"name": "a failing step",
|
||||
"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",
|
||||
},
|
||||
{
|
||||
"description": "",
|
||||
"keyword": "Scenario Outline",
|
||||
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}],
|
||||
"tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
|
||||
"steps": [
|
||||
{
|
||||
"line": 16,
|
||||
"line": 19,
|
||||
"match": {"location": ""},
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
"keyword": "Given",
|
||||
"name": "type str and value hello",
|
||||
}
|
||||
],
|
||||
"line": 15,
|
||||
"line": 18,
|
||||
"type": "scenario",
|
||||
"id": "test_passing_outline[str-hello]",
|
||||
"name": "Passing outline",
|
||||
|
@ -189,17 +192,17 @@ def test_step_trace(pytester):
|
|||
{
|
||||
"description": "",
|
||||
"keyword": "Scenario Outline",
|
||||
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}],
|
||||
"tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
|
||||
"steps": [
|
||||
{
|
||||
"line": 16,
|
||||
"line": 19,
|
||||
"match": {"location": ""},
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
"keyword": "Given",
|
||||
"name": "type int and value 42",
|
||||
}
|
||||
],
|
||||
"line": 15,
|
||||
"line": 18,
|
||||
"type": "scenario",
|
||||
"id": "test_passing_outline[int-42]",
|
||||
"name": "Passing outline",
|
||||
|
@ -207,17 +210,17 @@ def test_step_trace(pytester):
|
|||
{
|
||||
"description": "",
|
||||
"keyword": "Scenario Outline",
|
||||
"tags": [{"line": 14, "name": "scenario-outline-passing-tag"}],
|
||||
"tags": [{"line": 17, "name": "scenario-outline-passing-tag"}],
|
||||
"steps": [
|
||||
{
|
||||
"line": 16,
|
||||
"line": 19,
|
||||
"match": {"location": ""},
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
"keyword": "Given",
|
||||
"name": "type float and value 1.0",
|
||||
}
|
||||
],
|
||||
"line": 15,
|
||||
"line": 18,
|
||||
"type": "scenario",
|
||||
"id": "test_passing_outline[float-1.0]",
|
||||
"name": "Passing outline",
|
||||
|
|
|
@ -320,3 +320,58 @@ def test_forward_slash_in_params(pytester):
|
|||
result = pytester.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
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",
|
||||
"line_number": 5,
|
||||
"name": "Passing",
|
||||
"description": "",
|
||||
"steps": [
|
||||
{
|
||||
"duration": OfType(float),
|
||||
|
@ -159,6 +160,7 @@ def test_step_trace(pytester):
|
|||
"keyword": "Scenario",
|
||||
"line_number": 10,
|
||||
"name": "Failing",
|
||||
"description": "",
|
||||
"steps": [
|
||||
{
|
||||
"duration": OfType(float),
|
||||
|
@ -197,6 +199,7 @@ def test_step_trace(pytester):
|
|||
"keyword": "Scenario Outline",
|
||||
"line_number": 14,
|
||||
"name": "Outlined",
|
||||
"description": "",
|
||||
"steps": [
|
||||
{
|
||||
"duration": OfType(float),
|
||||
|
@ -243,6 +246,7 @@ def test_step_trace(pytester):
|
|||
"keyword": "Scenario Outline",
|
||||
"line_number": 14,
|
||||
"name": "Outlined",
|
||||
"description": "",
|
||||
"steps": [
|
||||
{
|
||||
"duration": OfType(float),
|
||||
|
|
Loading…
Reference in New Issue