Merge branch 'master' into test-389
This commit is contained in:
commit
07ca9ef0ba
|
@ -14,6 +14,7 @@ Added
|
|||
|
||||
Changed
|
||||
+++++++
|
||||
* Step arguments ``"datatable"`` and ``"docstring"`` are now reserved, and they can't be used as step argument names.
|
||||
|
||||
Deprecated
|
||||
++++++++++
|
||||
|
@ -23,6 +24,8 @@ Removed
|
|||
|
||||
Fixed
|
||||
+++++
|
||||
* Fixed an issue with the upcoming pytest release related to the use of ``@pytest.mark.usefixtures`` with an empty list.
|
||||
* Render template variables in docstrings and datatable cells with example table entries, as we already do for steps definitions.
|
||||
|
||||
Security
|
||||
++++++++
|
||||
|
|
52
README.rst
52
README.rst
|
@ -513,6 +513,58 @@ Example:
|
|||
def should_have_left_cucumbers(cucumbers, left):
|
||||
assert cucumbers["start"] - cucumbers["eat"] == left
|
||||
|
||||
|
||||
Example parameters from example tables can not only be used in steps, but also embedded directly within docstrings and datatables, allowing for dynamic substitution.
|
||||
This provides added flexibility for scenarios that require complex setups or validations.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: gherkin
|
||||
|
||||
# content of docstring_and_datatable_with_params.feature
|
||||
|
||||
Feature: Docstring and Datatable with example parameters
|
||||
Scenario Outline: Using parameters in docstrings and datatables
|
||||
Given the following configuration:
|
||||
"""
|
||||
username: <username>
|
||||
password: <password>
|
||||
"""
|
||||
When the user logs in
|
||||
Then the response should contain:
|
||||
| field | value |
|
||||
| username | <username> |
|
||||
| logged_in | true |
|
||||
|
||||
Examples:
|
||||
| username | password |
|
||||
| user1 | pass123 |
|
||||
| user2 | 123secure |
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytest_bdd import scenarios, given, when, then
|
||||
import json
|
||||
|
||||
# Load scenarios from the feature file
|
||||
scenarios("docstring_and_datatable_with_params.feature")
|
||||
|
||||
|
||||
@given("the following configuration:")
|
||||
def given_user_config(docstring):
|
||||
print(docstring)
|
||||
|
||||
|
||||
@when("the user logs in")
|
||||
def user_logs_in(logged_in):
|
||||
logged_in = True
|
||||
|
||||
|
||||
@then("the response should contain:")
|
||||
def response_should_contain(datatable):
|
||||
assert datatable[1][1] in ["user1", "user2"]
|
||||
|
||||
|
||||
Rules
|
||||
-----
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
|
||||
class StepImplementationError(Exception):
|
||||
"""Step implementation error."""
|
||||
|
||||
|
||||
class ScenarioIsDecoratorOnly(Exception):
|
||||
"""Scenario can be only used as decorator."""
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os.path
|
||||
import re
|
||||
import textwrap
|
||||
|
@ -20,7 +21,28 @@ from .gherkin_parser import Tag as GherkinTag
|
|||
from .gherkin_parser import get_gherkin_document
|
||||
from .types import STEP_TYPE_BY_PARSER_KEYWORD
|
||||
|
||||
STEP_PARAM_RE = re.compile(r"<(.+?)>")
|
||||
PARAM_RE = re.compile(r"<(.+?)>")
|
||||
|
||||
|
||||
def render_string(input_string: str, render_context: Mapping[str, object]) -> str:
|
||||
"""
|
||||
Render the string with the given context,
|
||||
but avoid replacing text inside angle brackets if context is missing.
|
||||
|
||||
Args:
|
||||
input_string (str): The string for which to render/replace params.
|
||||
render_context (Mapping[str, object]): The context for rendering the string.
|
||||
|
||||
Returns:
|
||||
str: The rendered string with parameters replaced only if they exist in the context.
|
||||
"""
|
||||
|
||||
def replacer(m: re.Match) -> str:
|
||||
varname = m.group(1)
|
||||
# If the context contains the variable, replace it. Otherwise, leave it unchanged.
|
||||
return str(render_context.get(varname, f"<{varname}>"))
|
||||
|
||||
return PARAM_RE.sub(replacer, input_string)
|
||||
|
||||
|
||||
def get_tag_names(tag_data: list[GherkinTag]) -> set[str]:
|
||||
|
@ -189,25 +211,25 @@ class ScenarioTemplate:
|
|||
Returns:
|
||||
Scenario: A Scenario object with steps rendered based on the context.
|
||||
"""
|
||||
base_steps = self.all_background_steps + self._steps
|
||||
scenario_steps = [
|
||||
Step(
|
||||
name=step.render(context),
|
||||
name=render_string(step.name, context),
|
||||
type=step.type,
|
||||
indent=step.indent,
|
||||
line_number=step.line_number,
|
||||
keyword=step.keyword,
|
||||
datatable=step.datatable,
|
||||
docstring=step.docstring,
|
||||
datatable=step.render_datatable(step.datatable, context) if step.datatable else None,
|
||||
docstring=render_string(step.docstring, context) if step.docstring else None,
|
||||
)
|
||||
for step in self._steps
|
||||
for step in base_steps
|
||||
]
|
||||
steps = self.all_background_steps + scenario_steps
|
||||
return Scenario(
|
||||
feature=self.feature,
|
||||
keyword=self.keyword,
|
||||
name=self.name,
|
||||
name=render_string(self.name, context),
|
||||
line_number=self.line_number,
|
||||
steps=steps,
|
||||
steps=scenario_steps,
|
||||
tags=self.tags,
|
||||
description=self.description,
|
||||
rule=self.rule,
|
||||
|
@ -299,31 +321,24 @@ class Step:
|
|||
"""
|
||||
return f'{self.type.capitalize()} "{self.name}"'
|
||||
|
||||
@property
|
||||
def params(self) -> tuple[str, ...]:
|
||||
"""Get the parameters in the step name.
|
||||
|
||||
Returns:
|
||||
Tuple[str, ...]: A tuple of parameter names found in the step name.
|
||||
@staticmethod
|
||||
def render_datatable(datatable: DataTable, context: Mapping[str, object]) -> DataTable:
|
||||
"""
|
||||
return tuple(frozenset(STEP_PARAM_RE.findall(self.name)))
|
||||
|
||||
def render(self, context: Mapping[str, Any]) -> str:
|
||||
"""Render the step name with the given context, but avoid replacing text inside angle brackets if context is missing.
|
||||
Render the datatable with the given context,
|
||||
but avoid replacing text inside angle brackets if context is missing.
|
||||
|
||||
Args:
|
||||
context (Mapping[str, Any]): The context for rendering the step name.
|
||||
datatable (DataTable): The datatable to render.
|
||||
context (Mapping[str, Any]): The context for rendering the datatable.
|
||||
|
||||
Returns:
|
||||
str: The rendered step name with parameters replaced only if they exist in the context.
|
||||
datatable (DataTable): The rendered datatable with parameters replaced only if they exist in the context.
|
||||
"""
|
||||
|
||||
def replacer(m: re.Match) -> str:
|
||||
varname = m.group(1)
|
||||
# If the context contains the variable, replace it. Otherwise, leave it unchanged.
|
||||
return str(context.get(varname, f"<{varname}>"))
|
||||
|
||||
return STEP_PARAM_RE.sub(replacer, self.name)
|
||||
rendered_datatable = copy.deepcopy(datatable)
|
||||
for row in rendered_datatable.rows:
|
||||
for cell in row.cells:
|
||||
cell.value = render_string(cell.value, context)
|
||||
return rendered_datatable
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
|
|
|
@ -18,6 +18,7 @@ import logging
|
|||
import os
|
||||
import re
|
||||
from collections.abc import Iterable, Iterator
|
||||
from inspect import signature
|
||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
|
||||
|
||||
import pytest
|
||||
|
@ -28,7 +29,7 @@ from . import exceptions
|
|||
from .compat import getfixturedefs, inject_fixture
|
||||
from .feature import get_feature, get_features
|
||||
from .steps import StepFunctionContext, get_step_fixture_name
|
||||
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
||||
from .utils import CONFIG_STACK, get_caller_module_locals, get_caller_module_path, get_required_args, identity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.mark.structures import ParameterSet
|
||||
|
@ -41,10 +42,13 @@ T = TypeVar("T")
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
||||
ALPHA_REGEX = re.compile(r"^\d+_*")
|
||||
|
||||
STEP_ARGUMENT_DATATABLE = "datatable"
|
||||
STEP_ARGUMENT_DOCSTRING = "docstring"
|
||||
STEP_ARGUMENTS_RESERVED_NAMES = {STEP_ARGUMENT_DATATABLE, STEP_ARGUMENT_DOCSTRING}
|
||||
|
||||
|
||||
def find_fixturedefs_for_step(step: Step, fixturemanager: FixtureManager, node: Node) -> Iterable[FixtureDef[Any]]:
|
||||
"""Find the fixture defs that can parse a step."""
|
||||
|
@ -172,11 +176,35 @@ def get_step_function(request: FixtureRequest, step: Step) -> StepFunctionContex
|
|||
return None
|
||||
|
||||
|
||||
def parse_step_arguments(step: Step, context: StepFunctionContext) -> dict[str, object]:
|
||||
"""Parse step arguments."""
|
||||
parsed_args = context.parser.parse_arguments(step.name)
|
||||
|
||||
assert parsed_args is not None, (
|
||||
f"Unexpected `NoneType` returned from " f"parse_arguments(...) in parser: {context.parser!r}"
|
||||
)
|
||||
|
||||
reserved_args = set(parsed_args.keys()) & STEP_ARGUMENTS_RESERVED_NAMES
|
||||
if reserved_args:
|
||||
reserved_arguments_str = ", ".join(repr(arg) for arg in reserved_args)
|
||||
raise exceptions.StepImplementationError(
|
||||
f"Step {step.name!r} defines argument names that are reserved: {reserved_arguments_str}. "
|
||||
"Please use different names."
|
||||
)
|
||||
|
||||
converted_args = {key: (context.converters.get(key, identity)(value)) for key, value in parsed_args.items()}
|
||||
|
||||
return converted_args
|
||||
|
||||
|
||||
def _execute_step_function(
|
||||
request: FixtureRequest, scenario: Scenario, step: Step, context: StepFunctionContext
|
||||
) -> None:
|
||||
"""Execute step function."""
|
||||
__tracebackhide__ = True
|
||||
|
||||
func_sig = signature(context.step_func)
|
||||
|
||||
kw = {
|
||||
"request": request,
|
||||
"feature": scenario.feature,
|
||||
|
@ -185,38 +213,32 @@ def _execute_step_function(
|
|||
"step_func": context.step_func,
|
||||
"step_func_args": {},
|
||||
}
|
||||
|
||||
request.config.hook.pytest_bdd_before_step(**kw)
|
||||
|
||||
# Get the step argument values.
|
||||
converters = context.converters
|
||||
kwargs = {}
|
||||
args = get_args(context.step_func)
|
||||
|
||||
try:
|
||||
parsed_args = context.parser.parse_arguments(step.name)
|
||||
assert parsed_args is not None, (
|
||||
f"Unexpected `NoneType` returned from " f"parse_arguments(...) in parser: {context.parser!r}"
|
||||
)
|
||||
parsed_args = parse_step_arguments(step=step, context=context)
|
||||
|
||||
for arg, value in parsed_args.items():
|
||||
if arg in converters:
|
||||
value = converters[arg](value)
|
||||
kwargs[arg] = value
|
||||
# Filter out the arguments that are not in the function signature
|
||||
kwargs = {k: v for k, v in parsed_args.items() if k in func_sig.parameters}
|
||||
|
||||
if step.datatable is not None:
|
||||
kwargs["datatable"] = step.datatable.raw()
|
||||
if STEP_ARGUMENT_DATATABLE in func_sig.parameters and step.datatable is not None:
|
||||
kwargs[STEP_ARGUMENT_DATATABLE] = step.datatable.raw()
|
||||
if STEP_ARGUMENT_DOCSTRING in func_sig.parameters and step.docstring is not None:
|
||||
kwargs[STEP_ARGUMENT_DOCSTRING] = step.docstring
|
||||
|
||||
if step.docstring is not None:
|
||||
kwargs["docstring"] = step.docstring
|
||||
|
||||
kwargs = {arg: kwargs[arg] if arg in kwargs else request.getfixturevalue(arg) for arg in args}
|
||||
# Fill the missing arguments requesting the fixture values
|
||||
kwargs |= {
|
||||
arg: request.getfixturevalue(arg) for arg in get_required_args(context.step_func) if arg not in kwargs
|
||||
}
|
||||
|
||||
kw["step_func_args"] = kwargs
|
||||
|
||||
request.config.hook.pytest_bdd_before_step_call(**kw)
|
||||
# Execute the step as if it was a pytest fixture, so that we can allow "yield" statements in it
|
||||
|
||||
# Execute the step as if it was a pytest fixture using `call_fixture_func`,
|
||||
# so that we can allow "yield" statements in it
|
||||
return_value = call_fixture_func(fixturefunc=context.step_func, request=request, kwargs=kwargs)
|
||||
|
||||
except Exception as exception:
|
||||
request.config.hook.pytest_bdd_step_error(exception=exception, **kw)
|
||||
raise
|
||||
|
@ -269,11 +291,8 @@ def _get_scenario_decorator(
|
|||
"scenario function can only be used as a decorator. Refer to the documentation."
|
||||
)
|
||||
[fn] = args
|
||||
func_args = get_args(fn)
|
||||
func_args = get_required_args(fn)
|
||||
|
||||
# We need to tell pytest that the original function requires its fixtures,
|
||||
# otherwise indirect fixtures would not work.
|
||||
@pytest.mark.usefixtures(*func_args)
|
||||
def scenario_wrapper(request: FixtureRequest, _pytest_bdd_example: dict[str, str]) -> Any:
|
||||
__tracebackhide__ = True
|
||||
scenario = templated_scenario.render(_pytest_bdd_example)
|
||||
|
@ -281,6 +300,11 @@ def _get_scenario_decorator(
|
|||
fixture_values = [request.getfixturevalue(arg) for arg in func_args]
|
||||
return fn(*fixture_values)
|
||||
|
||||
if func_args:
|
||||
# We need to tell pytest that the original function requires its fixtures,
|
||||
# otherwise indirect fixtures would not work.
|
||||
scenario_wrapper = pytest.mark.usefixtures(*func_args)(scenario_wrapper)
|
||||
|
||||
example_parametrizations = collect_example_parametrizations(templated_scenario)
|
||||
if example_parametrizations is not None:
|
||||
# Parametrize the scenario outlines
|
||||
|
@ -295,7 +319,7 @@ def _get_scenario_decorator(
|
|||
config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper)
|
||||
|
||||
scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}"
|
||||
scenario_wrapper.__scenario__ = templated_scenario
|
||||
scenario_wrapper.__scenario__ = templated_scenario # type: ignore[attr-defined]
|
||||
return cast(Callable[P, T], scenario_wrapper)
|
||||
|
||||
return decorator
|
||||
|
|
|
@ -20,13 +20,12 @@ T = TypeVar("T")
|
|||
CONFIG_STACK: list[Config] = []
|
||||
|
||||
|
||||
def get_args(func: Callable[..., Any]) -> list[str]:
|
||||
"""Get a list of argument names for a function.
|
||||
def get_required_args(func: Callable[..., Any]) -> list[str]:
|
||||
"""Get a list of argument that are required for a function.
|
||||
|
||||
:param func: The function to inspect.
|
||||
|
||||
:return: A list of argument names.
|
||||
:rtype: list
|
||||
"""
|
||||
params = signature(func).parameters.values()
|
||||
return [
|
||||
|
@ -83,3 +82,8 @@ def setdefault(obj: object, name: str, default: T) -> T:
|
|||
except AttributeError:
|
||||
setattr(obj, name, default)
|
||||
return default
|
||||
|
||||
|
||||
def identity(x: T) -> T:
|
||||
"""Return the argument."""
|
||||
return x
|
||||
|
|
|
@ -210,3 +210,48 @@ def test_steps_with_datatable_missing_argument_in_step(pytester):
|
|||
)
|
||||
result = pytester.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
def test_datatable_step_argument_is_reserved_and_cannot_be_used(pytester):
|
||||
pytester.makefile(
|
||||
".feature",
|
||||
reserved_datatable_arg=textwrap.dedent(
|
||||
"""\
|
||||
Feature: Reserved datatable argument
|
||||
|
||||
Scenario: Reserved datatable argument
|
||||
Given this step has a {datatable} argument
|
||||
Then the test fails
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
pytester.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd import scenario, given, then, parsers
|
||||
|
||||
@scenario("reserved_datatable_arg.feature", "Reserved datatable argument")
|
||||
def test_datatable():
|
||||
pass
|
||||
|
||||
|
||||
@given(parsers.parse("this step has a {datatable} argument"))
|
||||
def _(datatable):
|
||||
pass
|
||||
|
||||
|
||||
@then("the test fails")
|
||||
def _():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
result = pytester.runpytest()
|
||||
result.assert_outcomes(failed=1)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*Step 'this step has a {datatable} argument' defines argument names that are reserved: 'datatable'. Please use different names.*"
|
||||
]
|
||||
)
|
||||
|
|
|
@ -199,6 +199,120 @@ def test_angular_brackets_are_not_parsed(pytester):
|
|||
result.assert_outcomes(passed=2)
|
||||
|
||||
|
||||
def test_example_params(pytester):
|
||||
"""Test example params are rendered where necessary:
|
||||
* Step names
|
||||
* Docstring
|
||||
* Datatables
|
||||
"""
|
||||
pytester.makefile(
|
||||
".feature",
|
||||
example_params='''
|
||||
Feature: Example params
|
||||
Background:
|
||||
Given I have a background <background>
|
||||
And my background has:
|
||||
"""
|
||||
Background <background>
|
||||
"""
|
||||
|
||||
Scenario Outline: Outlined scenario
|
||||
Given I have a templated <foo>
|
||||
When I have a templated datatable
|
||||
| <data> |
|
||||
| example |
|
||||
And I have a templated docstring
|
||||
"""
|
||||
This is a <doc>
|
||||
"""
|
||||
Then pass
|
||||
|
||||
Examples:
|
||||
| background | foo | data | doc |
|
||||
| parameter | bar | table | string |
|
||||
''',
|
||||
)
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
from pytest_bdd import scenarios, given, when, then, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
scenarios("example_params.feature")
|
||||
|
||||
|
||||
@given(parsers.parse("I have a background {background}"))
|
||||
def _(background):
|
||||
return dump_obj(("background", background))
|
||||
|
||||
|
||||
@given(parsers.parse("I have a templated {foo}"))
|
||||
def _(foo):
|
||||
return "foo"
|
||||
|
||||
|
||||
@given("my background has:")
|
||||
def _(docstring):
|
||||
return dump_obj(("background_docstring", docstring))
|
||||
|
||||
|
||||
@given("I have a rule table:")
|
||||
def _(datatable):
|
||||
return dump_obj(("rule", datatable))
|
||||
|
||||
|
||||
@when("I have a templated datatable")
|
||||
def _(datatable):
|
||||
return dump_obj(("datatable", datatable))
|
||||
|
||||
|
||||
@when("I have a templated docstring")
|
||||
def _(docstring):
|
||||
return dump_obj(("docstring", docstring))
|
||||
|
||||
|
||||
@then("pass")
|
||||
def _():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
assert collect_dumped_objects(result) == [
|
||||
("background", "parameter"),
|
||||
("background_docstring", "Background parameter"),
|
||||
("datatable", [["table"], ["example"]]),
|
||||
("docstring", "This is a string"),
|
||||
]
|
||||
|
||||
|
||||
def test_step_parser_argument_not_in_function_signature_does_not_fail(pytester):
|
||||
"""Test that if the step parser defines an argument, but step function does not accept it,
|
||||
then it does not fail and the params is just not filled."""
|
||||
|
||||
pytester.makefile(
|
||||
".feature",
|
||||
simple="""
|
||||
Feature: Simple feature
|
||||
Scenario: Step with missing argument
|
||||
Given a user with username "user1"
|
||||
""",
|
||||
)
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
from pytest_bdd import scenarios, given, parsers
|
||||
|
||||
scenarios("simple.feature")
|
||||
|
||||
@given(parsers.parse('a user with username "{username}"'))
|
||||
def create_user():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
def test_multilanguage_support(pytester):
|
||||
"""Test multilanguage support."""
|
||||
pytester.makefile(
|
||||
|
@ -279,3 +393,36 @@ def test_multilanguage_support(pytester):
|
|||
("given", "che uso uno step con ", "esempio 2"),
|
||||
("then", "va tutto bene"),
|
||||
]
|
||||
|
||||
|
||||
def test_default_value_is_used_as_fallback(pytester):
|
||||
"""Test that the default value for a step implementation is only used as a fallback."""
|
||||
pytester.makefile(
|
||||
".feature",
|
||||
simple="""
|
||||
Feature: Simple feature
|
||||
Scenario: Step using default arg
|
||||
Given a user with default username
|
||||
|
||||
Scenario: Step using explicit value
|
||||
Given a user with username "user1"
|
||||
""",
|
||||
)
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
from pytest_bdd import scenarios, given, then, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
scenarios("simple.feature")
|
||||
|
||||
@given('a user with default username', target_fixture="user")
|
||||
@given(parsers.parse('a user with username "{username}"'), target_fixture="user")
|
||||
def create_user(username="defaultuser"):
|
||||
dump_obj(username)
|
||||
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-s")
|
||||
result.assert_outcomes(passed=2)
|
||||
|
||||
assert collect_dumped_objects(result) == ["defaultuser", "user1"]
|
||||
|
|
|
@ -193,3 +193,48 @@ def test_docstring_argument_in_step_impl_is_optional(pytester):
|
|||
)
|
||||
result = pytester.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
def test_docstring_step_argument_is_reserved_and_cannot_be_used(pytester):
|
||||
pytester.makefile(
|
||||
".feature",
|
||||
reserved_docstring_arg=textwrap.dedent(
|
||||
"""\
|
||||
Feature: Reserved docstring argument
|
||||
|
||||
Scenario: Reserved docstring argument
|
||||
Given this step has a {docstring} argument
|
||||
Then the test fails
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
pytester.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd import scenario, given, then, parsers
|
||||
|
||||
@scenario("reserved_docstring_arg.feature", "Reserved docstring argument")
|
||||
def test_docstring():
|
||||
pass
|
||||
|
||||
|
||||
@given(parsers.parse("this step has a {docstring} argument"))
|
||||
def _(docstring):
|
||||
pass
|
||||
|
||||
|
||||
@then("the test fails")
|
||||
def _():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
result = pytester.runpytest()
|
||||
result.assert_outcomes(failed=1)
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*Step 'this step has a {docstring} argument' defines argument names that are reserved: 'docstring'. Please use different names.*"
|
||||
]
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue