Rewrite scenario/feature examples logic (#445)
* rewrite examples subsitution using templating * Remove “example_converters” * Remove "expanded" option. It's now the default * Add utility functions to be able to inspect tests run by the pytester. * use better timer * Fix typos * Fix and simplify tests * Update to latest python 3.10 version * Add isort configuration and pre-commit hook * Fix imports * Fix types * Update changelog * Update README (mainly fix typos, remove outdated options) * Fix examples in README * Remove python2 junk Co-authored-by: Oleg Pidsadnyi <oleg.pidsadnyi@gmail.com>
This commit is contained in:
parent
c6b7134e59
commit
379cb4b47c
|
@ -11,7 +11,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.6", "3.7", "3.8", "3.9", 3.10.0-beta.4]
|
||||
python-version: ["3.6", "3.7", "3.8", "3.9", 3.10.0-rc.2]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
@ -2,9 +2,14 @@
|
|||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.6b0
|
||||
rev: 21.9b0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.9.3
|
||||
hooks:
|
||||
- id: isort
|
||||
name: isort (python)
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.0.1
|
||||
hooks:
|
||||
|
@ -13,7 +18,7 @@ repos:
|
|||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.19.4
|
||||
rev: v2.26.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py36-plus]
|
||||
|
|
|
@ -3,6 +3,12 @@ Changelog
|
|||
|
||||
Unreleased
|
||||
-----------
|
||||
This release introduces breaking changes, please refer to the :ref:`Migration from 4.x.x`.
|
||||
|
||||
- Rewrite the logic to parse Examples for Scenario Outlines. Now the substitution of the examples is done during the parsing of Gherkin feature files. You won't need to define the steps twice like ``@given("there are <start> cucumbers")`` and ``@given(parsers.parse("there are {start} cucumbers"))``. The latter will be enough.
|
||||
- Removed ``example_converters`` from ``scenario(...)`` signature. You should now use just the ``converters`` parameter for ``given``, ``when``, ``then``.
|
||||
- Removed ``--cucumberjson-expanded`` and ``--cucumber-json-expanded`` options. Now the JSON report is always expanded.
|
||||
- Removed ``--gherkin-terminal-reporter-expanded`` option. Now the terminal report is always expanded.
|
||||
|
||||
4.1.0
|
||||
-----------
|
||||
|
|
144
README.rst
144
README.rst
|
@ -109,15 +109,8 @@ test_publish_article.py:
|
|||
Scenario decorator
|
||||
------------------
|
||||
|
||||
The scenario decorator can accept the following optional keyword arguments:
|
||||
|
||||
* ``encoding`` - decode content of feature file in specific encoding. UTF-8 is default.
|
||||
* ``example_converters`` - mapping to pass functions to convert example values provided in feature files.
|
||||
|
||||
Functions decorated with the `scenario` decorator behave like a normal test function,
|
||||
and they will be executed after all scenario steps.
|
||||
You can consider it as a normal pytest test function, e.g. order fixtures there,
|
||||
call other functions and make assertions:
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -129,6 +122,9 @@ call other functions and make assertions:
|
|||
assert article.title in browser.html
|
||||
|
||||
|
||||
.. NOTE:: It is however encouraged to try as much as possible to have your logic only inside the Given, When, Then steps.
|
||||
|
||||
|
||||
Step aliases
|
||||
------------
|
||||
|
||||
|
@ -239,7 +235,7 @@ Example:
|
|||
.. code-block:: gherkin
|
||||
|
||||
Feature: Step arguments
|
||||
Scenario: Arguments for given, when, thens
|
||||
Scenario: Arguments for given, when, then
|
||||
Given there are 5 cucumbers
|
||||
|
||||
When I eat 3 cucumbers
|
||||
|
@ -256,7 +252,7 @@ The code will look like:
|
|||
from pytest_bdd import scenario, given, when, then, parsers
|
||||
|
||||
|
||||
@scenario("arguments.feature", "Arguments for given, when, thens")
|
||||
@scenario("arguments.feature", "Arguments for given, when, then")
|
||||
def test_arguments():
|
||||
pass
|
||||
|
||||
|
@ -292,7 +288,7 @@ You can implement your own step parser. It's interface is quite simple. The code
|
|||
|
||||
def __init__(self, name, **kwargs):
|
||||
"""Compile regex."""
|
||||
super(re, self).__init__(name)
|
||||
super().__init__(name)
|
||||
self.regex = re.compile(re.sub("%(.+)%", "(?P<\1>.+)", self.name), **kwargs)
|
||||
|
||||
def parse_arguments(self, name):
|
||||
|
@ -316,9 +312,9 @@ Step arguments are fixtures as well!
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Step arguments are injected into pytest `request` context as normal fixtures with the names equal to the names of the
|
||||
arguments. This opens a number of possibilies:
|
||||
arguments. This opens a number of possibilities:
|
||||
|
||||
* you can access step's argument as a fixture in other step function just by mentioning it as an argument (just like any othe pytest fixture)
|
||||
* you can access step's argument as a fixture in other step function just by mentioning it as an argument (just like any other pytest fixture)
|
||||
* if the name of the step argument clashes with existing fixture, it will be overridden by step's argument value; this way you can set/override the value for some fixture deeply inside of the fixture tree in a ad-hoc way by just choosing the proper name for the step argument.
|
||||
|
||||
|
||||
|
@ -433,7 +429,7 @@ step arguments and capture lines after first line (or some subset of them) into
|
|||
|
||||
import re
|
||||
|
||||
from pytest_bdd import given, then, scenario
|
||||
from pytest_bdd import given, then, scenario, parsers
|
||||
|
||||
|
||||
@scenario(
|
||||
|
@ -454,7 +450,7 @@ step arguments and capture lines after first line (or some subset of them) into
|
|||
assert i_have_text == text == 'Some\nExtra\nLines'
|
||||
|
||||
Note that `then` step definition (`text_should_be_correct`) in this example uses `text` fixture which is provided
|
||||
by a a `given` step (`i_have_text`) argument with the same name (`text`). This possibility is described in
|
||||
by a `given` step (`i_have_text`) argument with the same name (`text`). This possibility is described in
|
||||
the `Step arguments are fixtures as well!`_ section.
|
||||
|
||||
|
||||
|
@ -508,7 +504,7 @@ Scenario outlines
|
|||
-----------------
|
||||
|
||||
Scenarios can be parametrized to cover few cases. In Gherkin the variable
|
||||
templates are written using corner braces as <somevalue>.
|
||||
templates are written using corner braces as ``<somevalue>``.
|
||||
`Gherkin scenario outlines <http://behat.org/en/v3.0/user_guide/writing_scenarios.html#scenario-outlines>`_ are supported by pytest-bdd
|
||||
exactly as it's described in be behave_ docs.
|
||||
|
||||
|
@ -517,7 +513,7 @@ Example:
|
|||
.. code-block:: gherkin
|
||||
|
||||
Feature: Scenario outlines
|
||||
Scenario Outline: Outlined given, when, thens
|
||||
Scenario Outline: Outlined given, when, then
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
@ -532,7 +528,7 @@ pytest-bdd feature file format also supports example tables in different way:
|
|||
.. code-block:: gherkin
|
||||
|
||||
Feature: Scenario outlines
|
||||
Scenario Outline: Outlined given, when, thens
|
||||
Scenario Outline: Outlined given, when, then
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
@ -549,31 +545,30 @@ The code will look like:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
from pytest_bdd import given, when, then, scenario, parsers
|
||||
|
||||
|
||||
@scenario(
|
||||
"outline.feature",
|
||||
"Outlined given, when, thens",
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
"Outlined given, when, then",
|
||||
)
|
||||
def test_outlined():
|
||||
pass
|
||||
|
||||
|
||||
@given("there are <start> cucumbers", target_fixture="start_cucumbers")
|
||||
@given(parsers.parse("there are {start:d} cucumbers", target_fixture="start_cucumbers"))
|
||||
def start_cucumbers(start):
|
||||
assert isinstance(start, int)
|
||||
return dict(start=start)
|
||||
|
||||
|
||||
@when("I eat <eat> cucumbers")
|
||||
@when(parsers.parse("I eat {eat:g} cucumbers"))
|
||||
def eat_cucumbers(start_cucumbers, eat):
|
||||
assert isinstance(eat, float)
|
||||
start_cucumbers["eat"] = eat
|
||||
|
||||
|
||||
@then("I should have <left> cucumbers")
|
||||
@then(parsers.parse("I should have {left} cucumbers"))
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
assert isinstance(left, str)
|
||||
assert start - eat == int(left)
|
||||
|
@ -654,7 +649,7 @@ The code will look like:
|
|||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd import scenario, given, when, then, parsers
|
||||
|
||||
|
||||
# Here we use pytest to parametrize the test with the parameters table
|
||||
|
@ -664,7 +659,7 @@ The code will look like:
|
|||
)
|
||||
@scenario(
|
||||
"parametrized.feature",
|
||||
"Parametrized given, when, thens",
|
||||
"Parametrized given, when, then",
|
||||
)
|
||||
# Note that we should take the same arguments in the test function that we use
|
||||
# for the test parametrization either directly or indirectly (fixtures depend on them).
|
||||
|
@ -672,17 +667,17 @@ The code will look like:
|
|||
"""We don't need to do anything here, everything will be managed by the scenario decorator."""
|
||||
|
||||
|
||||
@given("there are <start> cucumbers", target_fixture="start_cucumbers")
|
||||
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="start_cucumbers")
|
||||
def start_cucumbers(start):
|
||||
return dict(start=start)
|
||||
|
||||
|
||||
@when("I eat <eat> cucumbers")
|
||||
@when(parsers.parse("I eat {eat:d} cucumbers"))
|
||||
def eat_cucumbers(start_cucumbers, start, eat):
|
||||
start_cucumbers["eat"] = eat
|
||||
|
||||
|
||||
@then("I should have <left> cucumbers")
|
||||
@then(parsers.parse("I should have {left:d} cucumbers"))
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
assert start - eat == left
|
||||
assert start_cucumbers["start"] == start
|
||||
|
@ -694,7 +689,7 @@ With a parametrized.feature file:
|
|||
.. code-block:: gherkin
|
||||
|
||||
Feature: parametrized
|
||||
Scenario: Parametrized given, when, thens
|
||||
Scenario: Parametrized given, when, then
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
@ -773,12 +768,12 @@ scenario test, so we can use standard test selection:
|
|||
|
||||
pytest -m "backend and login and successful"
|
||||
|
||||
The feature and scenario markers are not different from standard pytest markers, and the `@` symbol is stripped out
|
||||
The feature and scenario markers are not different from standard pytest markers, and the ``@`` symbol is stripped out
|
||||
automatically to allow test selector expressions. If you want to have bdd-related tags to be distinguishable from the
|
||||
other test markers, use prefix like `bdd`.
|
||||
Note that if you use pytest `--strict` option, all bdd tags mentioned in the feature files should be also in the
|
||||
`markers` setting of the `pytest.ini` config. Also for tags please use names which are python-compartible variable
|
||||
names, eg starts with a non-number, underscore alphanumberic, etc. That way you can safely use tags for tests filtering.
|
||||
`markers` setting of the `pytest.ini` config. Also for tags please use names which are python-compatible variable
|
||||
names, eg starts with a non-number, underscore alphanumeric, etc. That way you can safely use tags for tests filtering.
|
||||
|
||||
You can customize how tags are converted to pytest marks by implementing the
|
||||
``pytest_bdd_apply_tag`` hook and returning ``True`` from it:
|
||||
|
@ -791,7 +786,7 @@ You can customize how tags are converted to pytest marks by implementing the
|
|||
marker(function)
|
||||
return True
|
||||
else:
|
||||
# Fall back to pytest-bdd's default behavior
|
||||
# Fall back to the default behavior of pytest-bdd
|
||||
return None
|
||||
|
||||
Test setup
|
||||
|
@ -978,23 +973,7 @@ test_common.py:
|
|||
pass
|
||||
|
||||
There are no definitions of the steps in the test file. They were
|
||||
collected from the parent conftests.
|
||||
|
||||
|
||||
Using unicode in the feature files
|
||||
----------------------------------
|
||||
|
||||
As mentioned above, by default, utf-8 encoding is used for parsing feature files.
|
||||
For steps definition, you should use unicode strings, which is the default in python 3.
|
||||
If you are on python 2, make sure you use unicode strings by prefixing them with the `u` sign.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@given(parsers.re(u"у мене є рядок який містить '{0}'".format(u'(?P<content>.+)')))
|
||||
def there_is_a_string_with_content(content, string):
|
||||
"""Create string with unicode content."""
|
||||
string["content"] = content
|
||||
collected from the parent conftest.py.
|
||||
|
||||
|
||||
Default steps
|
||||
|
@ -1050,7 +1029,7 @@ The `features_base_dir` parameter can also be passed to the `@scenario` decorato
|
|||
Avoid retyping the feature file name
|
||||
------------------------------------
|
||||
|
||||
If you want to avoid retyping the feature file name when defining your scenarios in a test file, use functools.partial.
|
||||
If you want to avoid retyping the feature file name when defining your scenarios in a test file, use ``functools.partial``.
|
||||
This will make your life much easier when defining multiple scenarios in a test file. For example:
|
||||
|
||||
test_publish_article.py:
|
||||
|
@ -1118,8 +1097,8 @@ Reporting
|
|||
|
||||
It's important to have nice reporting out of your bdd tests. Cucumber introduced some kind of standard for
|
||||
`json format <https://www.relishapp.com/cucumber/cucumber/docs/json-output-formatter>`_
|
||||
which can be used for `this <https://wiki.jenkins-ci.org/display/JENKINS/Cucumber+Test+Result+Plugin>`_ jenkins
|
||||
plugin
|
||||
which can be used for, for example, by `this <https://plugins.jenkins.io/cucumber-testresult-plugin/>`_ Jenkins
|
||||
plugin.
|
||||
|
||||
To have an output in json format:
|
||||
|
||||
|
@ -1128,11 +1107,6 @@ To have an output in json format:
|
|||
pytest --cucumberjson=<path to json report>
|
||||
|
||||
This will output an expanded (meaning scenario outlines will be expanded to several scenarios) cucumber format.
|
||||
To also fill in parameters in the step name, you have to explicitly tell pytest-bdd to use the expanded format:
|
||||
|
||||
::
|
||||
|
||||
pytest --cucumberjson=<path to json report> --cucumberjson-expanded
|
||||
|
||||
To enable gherkin-formatted output on terminal, use
|
||||
|
||||
|
@ -1141,14 +1115,6 @@ To enable gherkin-formatted output on terminal, use
|
|||
pytest --gherkin-terminal-reporter
|
||||
|
||||
|
||||
Terminal reporter supports expanded format as well
|
||||
|
||||
::
|
||||
|
||||
pytest --gherkin-terminal-reporter-expanded
|
||||
|
||||
|
||||
|
||||
Test code generation helpers
|
||||
----------------------------
|
||||
|
||||
|
@ -1208,6 +1174,51 @@ As as side effect, the tool will validate the files for format errors, also some
|
|||
ordering of the types of the steps.
|
||||
|
||||
|
||||
.. _Migration from 4.x.x:
|
||||
|
||||
Migration of your tests from versions 4.x.x
|
||||
-------------------------------------------
|
||||
|
||||
Templated steps (e.g. ``@given("there are <start> cucumbers")``) should now the use step argument parsers in order to match the scenario outlines and get the values from the example tables. The values from the example tables are no longer passed as fixtures, although if you define your step to use a parser, the parameters will be still provided as fixtures.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Old step definition:
|
||||
@given("there are <start> cucumbers")
|
||||
def given_cucumbers(start):
|
||||
pass
|
||||
|
||||
|
||||
# New step definition:
|
||||
@given(parsers.parse("there are {start} cucumbers"))
|
||||
def given_cucumbers(start):
|
||||
pass
|
||||
|
||||
|
||||
Scenario `example_converters` are removed in favor of the converters provided on the step level:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Old code:
|
||||
@given("there are <start> cucumbers")
|
||||
def given_cucumbers(start):
|
||||
return {"start": start}
|
||||
|
||||
@scenario("outline.feature", "Outlined", example_converters={"start": float})
|
||||
def test_outline():
|
||||
pass
|
||||
|
||||
|
||||
# New code:
|
||||
@given(parsers.parse("there are {start} cucumbers"), converters={"start": float})
|
||||
def given_cucumbers(start):
|
||||
return {"start": start}
|
||||
|
||||
@scenario("outline.feature", "Outlined")
|
||||
def test_outline():
|
||||
pass
|
||||
|
||||
|
||||
.. _Migration from 3.x.x:
|
||||
|
||||
Migration of your tests from versions 3.x.x
|
||||
|
@ -1240,7 +1251,6 @@ as well as ``bdd_strict_gherkin`` from the ini files.
|
|||
|
||||
Step validation handlers for the hook ``pytest_bdd_step_validation_error`` should be removed.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
|
|
|
@ -5,3 +5,8 @@ build-backend = "setuptools.build_meta"
|
|||
[tool.black]
|
||||
line-length = 120
|
||||
target-version = ['py36', 'py37', 'py38']
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 120
|
||||
multi_line_output = 3
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""pytest-bdd public API."""
|
||||
|
||||
from pytest_bdd.steps import given, when, then
|
||||
from pytest_bdd.scenario import scenario, scenarios
|
||||
from pytest_bdd.steps import given, then, when
|
||||
|
||||
__version__ = "4.1.0"
|
||||
|
||||
|
|
|
@ -19,21 +19,12 @@ def add_options(parser):
|
|||
help="create cucumber json style report file at given path.",
|
||||
)
|
||||
|
||||
group._addoption(
|
||||
"--cucumberjson-expanded",
|
||||
"--cucumber-json-expanded",
|
||||
action="store_true",
|
||||
dest="expand",
|
||||
default=False,
|
||||
help="expand scenario outlines into scenarios and fill in the step names",
|
||||
)
|
||||
|
||||
|
||||
def configure(config):
|
||||
cucumber_json_path = config.option.cucumber_json_path
|
||||
# prevent opening json log on worker nodes (xdist)
|
||||
if cucumber_json_path and not hasattr(config, "workerinput"):
|
||||
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path, expand=config.option.expand)
|
||||
config._bddcucumberjson = LogBDDCucumberJSON(cucumber_json_path)
|
||||
config.pluginmanager.register(config._bddcucumberjson)
|
||||
|
||||
|
||||
|
@ -48,11 +39,10 @@ class LogBDDCucumberJSON:
|
|||
|
||||
"""Logging plugin for cucumber like json output."""
|
||||
|
||||
def __init__(self, logfile, expand=False):
|
||||
def __init__(self, logfile):
|
||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
||||
self.features = {}
|
||||
self.expand = expand
|
||||
|
||||
def append(self, obj):
|
||||
self.features[-1].append(obj)
|
||||
|
@ -88,23 +78,6 @@ class LogBDDCucumberJSON:
|
|||
"""
|
||||
return [{"name": tag, "line": item["line_number"] - 1} for tag in item["tags"]]
|
||||
|
||||
def _format_name(self, name, keys, values):
|
||||
for param, value in zip(keys, values):
|
||||
name = name.replace(f"<{param}>", str(value))
|
||||
return name
|
||||
|
||||
def _format_step_name(self, report, step):
|
||||
examples = report.scenario["examples"]
|
||||
if len(examples) == 0:
|
||||
return step["name"]
|
||||
|
||||
# we take the keys from the first "examples", but in each table, the keys should
|
||||
# be the same anyway since all the variables need to be filled in.
|
||||
keys, values = examples[0]["rows"]
|
||||
row_index = examples[0]["row_index"]
|
||||
|
||||
return self._format_name(step["name"], keys, values[row_index])
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
try:
|
||||
scenario = report.scenario
|
||||
|
@ -122,13 +95,7 @@ class LogBDDCucumberJSON:
|
|||
scenario["failed"] = True
|
||||
error_message = True
|
||||
|
||||
if self.expand:
|
||||
# XXX The format is already 'expanded' (scenario oultines -> scenarios),
|
||||
# but the step names were not filled in with parameters. To be backwards
|
||||
# compatible, do not fill in the step names unless explicitly asked for.
|
||||
step_name = self._format_step_name(report, step)
|
||||
else:
|
||||
step_name = step["name"]
|
||||
step_name = step["name"]
|
||||
|
||||
return {
|
||||
"keyword": step["keyword"],
|
||||
|
|
|
@ -20,20 +20,21 @@ Syntax example:
|
|||
And the article should be published # Note: will query the database
|
||||
|
||||
:note: The "#" symbol is used for comments.
|
||||
:note: There're no multiline steps, the description of the step must fit in
|
||||
:note: There are no multiline steps, the description of the step must fit in
|
||||
one line.
|
||||
"""
|
||||
import os.path
|
||||
import typing
|
||||
|
||||
import glob2
|
||||
|
||||
from .parser import parse_feature
|
||||
from .parser import Feature, parse_feature
|
||||
|
||||
# Global features dictionary
|
||||
features = {}
|
||||
features: typing.Dict[str, Feature] = {}
|
||||
|
||||
|
||||
def get_feature(base_path, filename, encoding="utf-8"):
|
||||
def get_feature(base_path: str, filename: str, encoding: str = "utf-8") -> Feature:
|
||||
"""Get a feature by the filename.
|
||||
|
||||
:param str base_path: Base feature directory.
|
||||
|
@ -55,7 +56,7 @@ def get_feature(base_path, filename, encoding="utf-8"):
|
|||
return feature
|
||||
|
||||
|
||||
def get_features(paths, **kwargs):
|
||||
def get_features(paths: typing.List[str], **kwargs) -> typing.List[Feature]:
|
||||
"""Get features for given paths.
|
||||
|
||||
:param list paths: `list` of paths (file or dirs)
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import re
|
||||
|
||||
from _pytest.terminal import TerminalReporter
|
||||
|
||||
from .parser import STEP_PARAM_RE
|
||||
|
||||
|
||||
def add_options(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
|
@ -12,14 +8,7 @@ def add_options(parser):
|
|||
action="store_true",
|
||||
dest="gherkin_terminal_reporter",
|
||||
default=False,
|
||||
help=("enable gherkin output"),
|
||||
)
|
||||
group._addoption(
|
||||
"--gherkin-terminal-reporter-expanded",
|
||||
action="store_true",
|
||||
dest="expand",
|
||||
default=False,
|
||||
help="expand scenario outlines into scenarios and fill in the step names",
|
||||
help="enable gherkin output",
|
||||
)
|
||||
|
||||
|
||||
|
@ -93,24 +82,9 @@ class GherkinTerminalReporter(TerminalReporter):
|
|||
self._tw.write(report.scenario["name"], **scenario_markup)
|
||||
self._tw.write("\n")
|
||||
for step in report.scenario["steps"]:
|
||||
if self.config.option.expand:
|
||||
step_name = self._format_step_name(step["name"], **report.scenario["example_kwargs"])
|
||||
else:
|
||||
step_name = step["name"]
|
||||
self._tw.write(" {} {}\n".format(step["keyword"], step_name), **scenario_markup)
|
||||
self._tw.write(f" {step['keyword']} {step['name']}\n", **scenario_markup)
|
||||
self._tw.write(" " + word, **word_markup)
|
||||
self._tw.write("\n\n")
|
||||
else:
|
||||
return TerminalReporter.pytest_runtest_logreport(self, rep)
|
||||
self.stats.setdefault(cat, []).append(rep)
|
||||
|
||||
def _format_step_name(self, step_name, **example_kwargs):
|
||||
while True:
|
||||
param_match = re.search(STEP_PARAM_RE, step_name)
|
||||
if not param_match:
|
||||
break
|
||||
param_token = param_match.group(0)
|
||||
param_name = param_match.group(1)
|
||||
param_value = example_kwargs[param_name]
|
||||
step_name = step_name.replace(param_token, param_value)
|
||||
return step_name
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import io
|
||||
import os.path
|
||||
import re
|
||||
import textwrap
|
||||
import typing
|
||||
from collections import OrderedDict
|
||||
|
||||
from . import types, exceptions
|
||||
from . import exceptions, types
|
||||
|
||||
SPLIT_LINE_RE = re.compile(r"(?<!\\)\|")
|
||||
STEP_PARAM_RE = re.compile(r"<(.+?)>")
|
||||
COMMENT_RE = re.compile(r"(^|(?<=\s))#")
|
||||
STEP_PREFIXES = [
|
||||
("Feature: ", types.FEATURE),
|
||||
|
@ -73,7 +74,7 @@ def get_step_type(line):
|
|||
return _type
|
||||
|
||||
|
||||
def parse_feature(basedir, filename, encoding="utf-8"):
|
||||
def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> "Feature":
|
||||
"""Parse the feature file.
|
||||
|
||||
:param str basedir: Feature files base directory.
|
||||
|
@ -93,10 +94,10 @@ def parse_feature(basedir, filename, encoding="utf-8"):
|
|||
background=None,
|
||||
description="",
|
||||
)
|
||||
scenario = None
|
||||
scenario: typing.Optional[ScenarioTemplate] = None
|
||||
mode = None
|
||||
prev_mode = None
|
||||
description = []
|
||||
description: typing.List[str] = []
|
||||
step = None
|
||||
multiline_step = False
|
||||
prev_line = None
|
||||
|
@ -149,7 +150,9 @@ def parse_feature(basedir, filename, encoding="utf-8"):
|
|||
keyword, parsed_line = parse_line(clean_line)
|
||||
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
|
||||
tags = get_tags(prev_line)
|
||||
feature.scenarios[parsed_line] = scenario = Scenario(feature, parsed_line, line_number, tags=tags)
|
||||
feature.scenarios[parsed_line] = scenario = ScenarioTemplate(
|
||||
feature=feature, name=parsed_line, line_number=line_number, tags=tags
|
||||
)
|
||||
elif mode == types.BACKGROUND:
|
||||
feature.background = Background(feature=feature, line_number=line_number)
|
||||
elif mode == types.EXAMPLES:
|
||||
|
@ -199,42 +202,35 @@ class Feature:
|
|||
"""Feature."""
|
||||
|
||||
def __init__(self, scenarios, filename, rel_filename, name, tags, examples, background, line_number, description):
|
||||
self.scenarios = scenarios
|
||||
self.scenarios: typing.Dict[str, ScenarioTemplate] = scenarios
|
||||
self.rel_filename = rel_filename
|
||||
self.filename = filename
|
||||
self.name = name
|
||||
self.tags = tags
|
||||
self.examples = examples
|
||||
self.name = name
|
||||
self.line_number = line_number
|
||||
self.tags = tags
|
||||
self.scenarios = scenarios
|
||||
self.description = description
|
||||
self.background = background
|
||||
|
||||
|
||||
class Scenario:
|
||||
class ScenarioTemplate:
|
||||
"""A scenario template.
|
||||
|
||||
"""Scenario."""
|
||||
Created when parsing the feature file, it will then be combined with the examples to create a Scenario."""
|
||||
|
||||
def __init__(self, feature, name, line_number, example_converters=None, tags=None):
|
||||
"""Scenario constructor.
|
||||
def __init__(self, feature: Feature, name: str, line_number: int, tags=None):
|
||||
"""
|
||||
|
||||
:param pytest_bdd.parser.Feature feature: Feature.
|
||||
:param str name: Scenario name.
|
||||
:param int line_number: Scenario line number.
|
||||
:param dict example_converters: Example table parameter converters.
|
||||
:param set tags: Set of tags.
|
||||
"""
|
||||
self.feature = feature
|
||||
self.name = name
|
||||
self._steps = []
|
||||
self._steps: typing.List[Step] = []
|
||||
self.examples = Examples()
|
||||
self.line_number = line_number
|
||||
self.example_converters = example_converters
|
||||
self.tags = tags or set()
|
||||
self.failed = False
|
||||
self.test_function = None
|
||||
|
||||
def add_step(self, step):
|
||||
"""Add step to the scenario.
|
||||
|
@ -246,41 +242,29 @@ class Scenario:
|
|||
|
||||
@property
|
||||
def steps(self):
|
||||
"""Get scenario steps including background steps.
|
||||
background = self.feature.background
|
||||
return (background.steps if background else []) + self._steps
|
||||
|
||||
:return: List of steps.
|
||||
"""
|
||||
result = []
|
||||
if self.feature.background:
|
||||
result.extend(self.feature.background.steps)
|
||||
result.extend(self._steps)
|
||||
return result
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
"""Get parameter names.
|
||||
|
||||
:return: Parameter names.
|
||||
:rtype: frozenset
|
||||
"""
|
||||
return frozenset(sum((list(step.params) for step in self.steps), []))
|
||||
|
||||
def get_example_params(self):
|
||||
"""Get example parameter names."""
|
||||
return set(self.examples.example_params + self.feature.examples.example_params)
|
||||
|
||||
def get_params(self, builtin=False):
|
||||
"""Get converted example params."""
|
||||
for examples in [self.feature.examples, self.examples]:
|
||||
yield examples.get_params(self.example_converters, builtin=builtin)
|
||||
def render(self, context: typing.Mapping[str, typing.Any]) -> "Scenario":
|
||||
steps = [
|
||||
Step(
|
||||
name=templated_step.render(context),
|
||||
type=templated_step.type,
|
||||
indent=templated_step.indent,
|
||||
line_number=templated_step.line_number,
|
||||
keyword=templated_step.keyword,
|
||||
)
|
||||
for templated_step in self.steps
|
||||
]
|
||||
return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags)
|
||||
|
||||
def validate(self):
|
||||
"""Validate the scenario.
|
||||
|
||||
:raises ScenarioValidationError: when scenario is not valid
|
||||
"""
|
||||
params = self.params
|
||||
example_params = self.get_example_params()
|
||||
params = frozenset(sum((list(step.params) for step in self.steps), []))
|
||||
example_params = set(self.examples.example_params + self.feature.examples.example_params)
|
||||
if params and example_params and params != example_params:
|
||||
raise exceptions.ScenarioExamplesNotValidError(
|
||||
"""Scenario "{}" in the feature "{}" has not valid examples. """
|
||||
|
@ -290,6 +274,26 @@ class Scenario:
|
|||
)
|
||||
|
||||
|
||||
class Scenario:
|
||||
|
||||
"""Scenario."""
|
||||
|
||||
def __init__(self, feature: Feature, name: str, line_number: int, steps: "typing.List[Step]", tags=None):
|
||||
"""Scenario constructor.
|
||||
|
||||
:param pytest_bdd.parser.Feature feature: Feature.
|
||||
:param str name: Scenario name.
|
||||
:param int line_number: Scenario line number.
|
||||
:param set tags: Set of tags.
|
||||
"""
|
||||
self.feature = feature
|
||||
self.name = name
|
||||
self.steps = steps
|
||||
self.line_number = line_number
|
||||
self.tags = tags or set()
|
||||
self.failed = False
|
||||
|
||||
|
||||
class Step:
|
||||
|
||||
"""Step."""
|
||||
|
@ -352,6 +356,13 @@ class Step:
|
|||
"""Get step params."""
|
||||
return tuple(frozenset(STEP_PARAM_RE.findall(self.name)))
|
||||
|
||||
def render(self, context: typing.Mapping[str, typing.Any]):
|
||||
def replacer(m: typing.Match):
|
||||
varname = m.group(1)
|
||||
return str(context[varname])
|
||||
|
||||
return STEP_PARAM_RE.sub(replacer, self.name)
|
||||
|
||||
|
||||
class Background:
|
||||
|
||||
|
@ -412,11 +423,7 @@ class Examples:
|
|||
self.example_params.append(param)
|
||||
self.vertical_examples.append(values)
|
||||
|
||||
def get_params(self, converters, builtin=False):
|
||||
"""Get scenario pytest parametrization table.
|
||||
|
||||
:param converters: `dict` of converter functions to convert parameter values
|
||||
"""
|
||||
def as_contexts(self) -> typing.Iterable[typing.Dict[str, typing.Any]]:
|
||||
param_count = len(self.example_params)
|
||||
if self.vertical_examples and not self.examples:
|
||||
for value_index in range(len(self.vertical_examples[0])):
|
||||
|
@ -425,20 +432,15 @@ class Examples:
|
|||
example.append(self.vertical_examples[param_index][value_index])
|
||||
self.examples.append(example)
|
||||
|
||||
if self.examples:
|
||||
params = []
|
||||
for example in self.examples:
|
||||
example = list(example)
|
||||
for index, param in enumerate(self.example_params):
|
||||
raw_value = example[index]
|
||||
if converters and param in converters:
|
||||
value = converters[param](raw_value)
|
||||
if not builtin or value.__class__.__module__ in {"__builtin__", "builtins"}:
|
||||
example[index] = value
|
||||
params.append(example)
|
||||
return [self.example_params, params]
|
||||
else:
|
||||
return []
|
||||
if not self.examples:
|
||||
return
|
||||
|
||||
header, rows = self.example_params, self.examples
|
||||
|
||||
for row in rows:
|
||||
assert len(header) == len(row)
|
||||
|
||||
yield dict(zip(header, row))
|
||||
|
||||
def __bool__(self):
|
||||
"""Bool comparison."""
|
||||
|
@ -455,6 +457,3 @@ def get_tags(line):
|
|||
if not line or not line.strip().startswith("@"):
|
||||
return set()
|
||||
return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1}
|
||||
|
||||
|
||||
STEP_PARAM_RE = re.compile(r"\<(.+?)\>")
|
||||
|
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
import pytest
|
||||
|
||||
from . import cucumber_json
|
||||
from . import generation
|
||||
from . import gherkin_terminal_reporter
|
||||
from . import given, when, then
|
||||
from . import reporting
|
||||
from . import cucumber_json, generation, gherkin_terminal_reporter, given, reporting, then, when
|
||||
from .utils import CONFIG_STACK
|
||||
|
||||
|
||||
|
@ -25,6 +21,20 @@ def trace():
|
|||
pytest.set_trace()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _pytest_bdd_example():
|
||||
"""The current scenario outline parametrization.
|
||||
|
||||
This is used internally by pytest_bdd.
|
||||
|
||||
If no outline is used, we just return an empty dict to render
|
||||
the current template without any actual variable.
|
||||
Otherwise pytest_bdd will add all the context variables in this fixture
|
||||
from the example definitions in the feature file.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Add pytest-bdd options."""
|
||||
add_bdd_ini(parser)
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
"""Reporting functionality.
|
||||
|
||||
Collection of the scenario excecution statuses, timing and other information
|
||||
Collection of the scenario execution statuses, timing and other information
|
||||
that enriches the pytest test reporting.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from .utils import get_parametrize_markers_args
|
||||
|
||||
|
||||
class StepReport:
|
||||
"""Step excecution report."""
|
||||
"""Step execution report."""
|
||||
|
||||
failed = False
|
||||
stopped = None
|
||||
|
@ -21,12 +19,12 @@ class StepReport:
|
|||
:param pytest_bdd.parser.Step step: Step.
|
||||
"""
|
||||
self.step = step
|
||||
self.started = time.time()
|
||||
self.started = time.perf_counter()
|
||||
|
||||
def serialize(self):
|
||||
"""Serialize the step excecution report.
|
||||
"""Serialize the step execution report.
|
||||
|
||||
:return: Serialized step excecution report.
|
||||
:return: Serialized step execution report.
|
||||
:rtype: dict
|
||||
"""
|
||||
return {
|
||||
|
@ -41,16 +39,16 @@ class StepReport:
|
|||
def finalize(self, failed):
|
||||
"""Stop collecting information and finalize the report.
|
||||
|
||||
:param bool failed: Wheither the step excecution is failed.
|
||||
:param bool failed: Whether the step execution is failed.
|
||||
"""
|
||||
self.stopped = time.time()
|
||||
self.stopped = time.perf_counter()
|
||||
self.failed = failed
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""Step excecution duration.
|
||||
"""Step execution duration.
|
||||
|
||||
:return: Step excecution duration.
|
||||
:return: Step execution duration.
|
||||
:rtype: float
|
||||
"""
|
||||
if self.stopped is None:
|
||||
|
@ -70,21 +68,6 @@ class ScenarioReport:
|
|||
"""
|
||||
self.scenario = scenario
|
||||
self.step_reports = []
|
||||
self.param_index = None
|
||||
parametrize_args = get_parametrize_markers_args(node)
|
||||
if parametrize_args and scenario.examples:
|
||||
param_names = (
|
||||
parametrize_args[0] if isinstance(parametrize_args[0], (tuple, list)) else [parametrize_args[0]]
|
||||
)
|
||||
param_values = parametrize_args[1]
|
||||
node_param_values = [node.funcargs[param_name] for param_name in param_names]
|
||||
if node_param_values in param_values:
|
||||
self.param_index = param_values.index(node_param_values)
|
||||
elif tuple(node_param_values) in param_values:
|
||||
self.param_index = param_values.index(tuple(node_param_values))
|
||||
self.example_kwargs = {
|
||||
example_param: str(node.funcargs[example_param]) for example_param in scenario.get_example_params()
|
||||
}
|
||||
|
||||
@property
|
||||
def current_step_report(self):
|
||||
|
@ -104,7 +87,7 @@ class ScenarioReport:
|
|||
self.step_reports.append(step_report)
|
||||
|
||||
def serialize(self):
|
||||
"""Serialize scenario excecution report in order to transfer reportin from nodes in the distributed mode.
|
||||
"""Serialize scenario execution report in order to transfer reporting from nodes in the distributed mode.
|
||||
|
||||
:return: Serialized report.
|
||||
:rtype: dict
|
||||
|
@ -112,7 +95,6 @@ class ScenarioReport:
|
|||
scenario = self.scenario
|
||||
feature = scenario.feature
|
||||
|
||||
params = sum(scenario.get_params(builtin=True), []) if scenario.examples else None
|
||||
return {
|
||||
"steps": [step_report.serialize() for step_report in self.step_reports],
|
||||
"name": scenario.name,
|
||||
|
@ -126,17 +108,6 @@ class ScenarioReport:
|
|||
"description": feature.description,
|
||||
"tags": sorted(feature.tags),
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": scenario.examples.name,
|
||||
"line_number": scenario.examples.line_number,
|
||||
"rows": params,
|
||||
"row_index": self.param_index,
|
||||
}
|
||||
]
|
||||
if scenario.examples
|
||||
else [],
|
||||
"example_kwargs": self.example_kwargs,
|
||||
}
|
||||
|
||||
def fail(self):
|
||||
|
|
|
@ -13,6 +13,7 @@ test_publish_article = scenario(
|
|||
import collections
|
||||
import os
|
||||
import re
|
||||
import typing
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import FixtureLookupError
|
||||
|
@ -22,6 +23,11 @@ from .feature import get_feature, get_features
|
|||
from .steps import get_step_fixture_name, inject_fixture
|
||||
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from _pytest.mark.structures import ParameterSet
|
||||
|
||||
from .parser import Feature, Scenario, ScenarioTemplate
|
||||
|
||||
PYTHON_REPLACE_REGEX = re.compile(r"\W")
|
||||
ALPHA_REGEX = re.compile(r"^\d+_*")
|
||||
|
||||
|
@ -38,6 +44,8 @@ def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None)
|
|||
if not match:
|
||||
continue
|
||||
|
||||
# TODO: maybe `converters` should be part of the SterParser.__init__(),
|
||||
# and used by StepParser.parse_arguments() method
|
||||
converters = getattr(fixturedef.func, "converters", {})
|
||||
for arg, value in parser.parse_arguments(name).items():
|
||||
if arg in converters:
|
||||
|
@ -113,7 +121,7 @@ def _execute_step_function(request, scenario, step, step_func):
|
|||
raise
|
||||
|
||||
|
||||
def _execute_scenario(feature, scenario, request):
|
||||
def _execute_scenario(feature: "Feature", scenario: "Scenario", request):
|
||||
"""Execute the scenario.
|
||||
|
||||
:param feature: Feature.
|
||||
|
@ -141,7 +149,9 @@ def _execute_scenario(feature, scenario, request):
|
|||
FakeRequest = collections.namedtuple("FakeRequest", ["module"])
|
||||
|
||||
|
||||
def _get_scenario_decorator(feature, feature_name, scenario, scenario_name):
|
||||
def _get_scenario_decorator(
|
||||
feature: "Feature", feature_name: str, templated_scenario: "ScenarioTemplate", scenario_name: str
|
||||
):
|
||||
# HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception
|
||||
# when the decorator is misused.
|
||||
# Pytest inspect the signature to determine the required fixtures, and in that case it would look
|
||||
|
@ -155,39 +165,62 @@ def _get_scenario_decorator(feature, feature_name, scenario, scenario_name):
|
|||
)
|
||||
[fn] = args
|
||||
args = get_args(fn)
|
||||
function_args = list(args)
|
||||
for arg in scenario.get_example_params():
|
||||
if arg not in function_args:
|
||||
function_args.append(arg)
|
||||
|
||||
@pytest.mark.usefixtures(*function_args)
|
||||
def scenario_wrapper(request):
|
||||
# We need to tell pytest that the original function requires its fixtures,
|
||||
# otherwise indirect fixtures would not work.
|
||||
@pytest.mark.usefixtures(*args)
|
||||
def scenario_wrapper(request, _pytest_bdd_example):
|
||||
scenario = templated_scenario.render(_pytest_bdd_example)
|
||||
_execute_scenario(feature, scenario, request)
|
||||
return fn(*(request.getfixturevalue(arg) for arg in args))
|
||||
fixture_values = [request.getfixturevalue(arg) for arg in args]
|
||||
return fn(*fixture_values)
|
||||
|
||||
for param_set in scenario.get_params():
|
||||
if param_set:
|
||||
scenario_wrapper = pytest.mark.parametrize(*param_set)(scenario_wrapper)
|
||||
for tag in scenario.tags.union(feature.tags):
|
||||
example_parametrizations = collect_example_parametrizations(templated_scenario)
|
||||
if example_parametrizations is not None:
|
||||
# Parametrize the scenario outlines
|
||||
scenario_wrapper = pytest.mark.parametrize(
|
||||
"_pytest_bdd_example",
|
||||
example_parametrizations,
|
||||
)(scenario_wrapper)
|
||||
|
||||
for tag in templated_scenario.tags.union(feature.tags):
|
||||
config = CONFIG_STACK[-1]
|
||||
config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper)
|
||||
|
||||
scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}"
|
||||
scenario_wrapper.__scenario__ = scenario
|
||||
scenario.test_function = scenario_wrapper
|
||||
scenario_wrapper.__scenario__ = templated_scenario
|
||||
return scenario_wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=None, features_base_dir=None):
|
||||
def collect_example_parametrizations(
|
||||
templated_scenario: "ScenarioTemplate",
|
||||
) -> "typing.Optional[typing.List[ParameterSet]]":
|
||||
# We need to evaluate these iterators and store them as lists, otherwise
|
||||
# we won't be able to do the cartesian product later (the second iterator will be consumed)
|
||||
feature_contexts = list(templated_scenario.feature.examples.as_contexts())
|
||||
scenario_contexts = list(templated_scenario.examples.as_contexts())
|
||||
|
||||
contexts = [
|
||||
{**feature_context, **scenario_context}
|
||||
# We must make sure that we always have at least one element in each list, otherwise
|
||||
# the cartesian product will result in an empty list too, even if one of the 2 sets
|
||||
# is non empty.
|
||||
for feature_context in feature_contexts or [{}]
|
||||
for scenario_context in scenario_contexts or [{}]
|
||||
]
|
||||
if contexts == [{}]:
|
||||
return None
|
||||
return [pytest.param(context, id="-".join(context.values())) for context in contexts]
|
||||
|
||||
|
||||
def scenario(feature_name: str, scenario_name: str, encoding: str = "utf-8", features_base_dir=None):
|
||||
"""Scenario decorator.
|
||||
|
||||
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
|
||||
:param str scenario_name: Scenario name.
|
||||
:param str encoding: Feature file encoding.
|
||||
:param dict example_converters: optional `dict` of example converter function, where key is the name of the
|
||||
example parameter, and value is the converter function.
|
||||
"""
|
||||
|
||||
scenario_name = str(scenario_name)
|
||||
|
@ -207,13 +240,11 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
|
|||
f'Scenario "{scenario_name}" in feature "{feature_name}" in {feature.filename} is not found.'
|
||||
)
|
||||
|
||||
scenario.example_converters = example_converters
|
||||
|
||||
# Validate the scenario
|
||||
scenario.validate()
|
||||
|
||||
return _get_scenario_decorator(
|
||||
feature=feature, feature_name=feature_name, scenario=scenario, scenario_name=scenario_name
|
||||
feature=feature, feature_name=feature_name, templated_scenario=scenario, scenario_name=scenario_name
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import glob2
|
|||
|
||||
from .generation import generate_code, parse_feature_files
|
||||
|
||||
MIGRATE_REGEX = re.compile(r"\s?(\w+)\s\=\sscenario\((.+)\)", flags=re.MULTILINE)
|
||||
MIGRATE_REGEX = re.compile(r"\s?(\w+)\s=\sscenario\((.+)\)", flags=re.MULTILINE)
|
||||
|
||||
|
||||
def migrate_tests(args):
|
||||
|
|
|
@ -36,11 +36,10 @@ def given_beautiful_article(article):
|
|||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from _pytest.fixtures import FixtureDef
|
||||
|
||||
from .types import GIVEN, WHEN, THEN
|
||||
from .parsers import get_parser
|
||||
from .types import GIVEN, THEN, WHEN
|
||||
from .utils import get_caller_module_locals
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
"""Various utility functions."""
|
||||
|
||||
from inspect import getframeinfo
|
||||
from inspect import signature as _signature
|
||||
import base64
|
||||
import pickle
|
||||
import re
|
||||
import typing
|
||||
from inspect import getframeinfo, signature
|
||||
from sys import _getframe
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from _pytest.pytester import RunResult
|
||||
|
||||
CONFIG_STACK = []
|
||||
|
||||
|
||||
|
@ -15,14 +20,10 @@ def get_args(func):
|
|||
:return: A list of argument names.
|
||||
:rtype: list
|
||||
"""
|
||||
params = _signature(func).parameters.values()
|
||||
params = signature(func).parameters.values()
|
||||
return [param.name for param in params if param.kind == param.POSITIONAL_OR_KEYWORD]
|
||||
|
||||
|
||||
def get_parametrize_markers_args(node):
|
||||
return tuple(arg for mark in node.iter_markers("parametrize") for arg in mark.args)
|
||||
|
||||
|
||||
def get_caller_module_locals(depth=2):
|
||||
"""Get the caller module locals dictionary.
|
||||
|
||||
|
@ -40,3 +41,26 @@ def get_caller_module_path(depth=2):
|
|||
"""
|
||||
frame = _getframe(depth)
|
||||
return getframeinfo(frame, context=0).filename
|
||||
|
||||
|
||||
_DUMP_START = "_pytest_bdd_>>>"
|
||||
_DUMP_END = "<<<_pytest_bdd_"
|
||||
|
||||
|
||||
def dump_obj(*objects):
|
||||
"""Dump objects to stdout so that they can be inspected by the test suite."""
|
||||
for obj in objects:
|
||||
dump = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
|
||||
encoded = base64.b64encode(dump).decode("ascii")
|
||||
print(f"{_DUMP_START}{encoded}{_DUMP_END}")
|
||||
|
||||
|
||||
def collect_dumped_objects(result: "RunResult"):
|
||||
"""Parse all the objects dumped with `dump_object` from the result.
|
||||
|
||||
Note: You must run the result with output to stdout enabled.
|
||||
For example, using ``testdir.runpytest("-s")``.
|
||||
"""
|
||||
stdout = result.stdout.str() # pytest < 6.2, otherwise we could just do str(result.stdout)
|
||||
payloads = re.findall(rf"{_DUMP_START}(.*?){_DUMP_END}", stdout)
|
||||
return [pickle.loads(base64.b64decode(payload)) for payload in payloads]
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import textwrap
|
||||
|
||||
|
||||
FEATURE = """\
|
||||
Feature: Background support
|
||||
|
||||
|
|
|
@ -71,7 +71,7 @@ def test_step_trace(testdir):
|
|||
textwrap.dedent(
|
||||
"""
|
||||
import pytest
|
||||
from pytest_bdd import given, when, scenario
|
||||
from pytest_bdd import given, when, scenario, parsers
|
||||
|
||||
@given('a passing step')
|
||||
def a_passing_step():
|
||||
|
@ -85,7 +85,7 @@ def test_step_trace(testdir):
|
|||
def a_failing_step():
|
||||
raise Exception('Error')
|
||||
|
||||
@given('type <type> and value <value>')
|
||||
@given(parsers.parse('type {type} and value {value}'))
|
||||
def type_type_and_value_value():
|
||||
return 'pass'
|
||||
|
||||
|
@ -104,6 +104,8 @@ def test_step_trace(testdir):
|
|||
)
|
||||
)
|
||||
result, jsonobject = runandparse(testdir)
|
||||
result.assert_outcomes(passed=4, failed=1)
|
||||
|
||||
assert result.ret
|
||||
expected = [
|
||||
{
|
||||
|
@ -169,7 +171,7 @@ def test_step_trace(testdir):
|
|||
"match": {"location": ""},
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
"keyword": "Given",
|
||||
"name": "type <type> and value <value>",
|
||||
"name": "type str and value hello",
|
||||
}
|
||||
],
|
||||
"line": 15,
|
||||
|
@ -187,7 +189,7 @@ def test_step_trace(testdir):
|
|||
"match": {"location": ""},
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
"keyword": "Given",
|
||||
"name": "type <type> and value <value>",
|
||||
"name": "type int and value 42",
|
||||
}
|
||||
],
|
||||
"line": 15,
|
||||
|
@ -205,7 +207,7 @@ def test_step_trace(testdir):
|
|||
"match": {"location": ""},
|
||||
"result": {"status": "passed", "duration": OfType(int)},
|
||||
"keyword": "Given",
|
||||
"name": "type <type> and value <value>",
|
||||
"name": "type float and value 1.0",
|
||||
}
|
||||
],
|
||||
"line": 15,
|
||||
|
@ -224,102 +226,3 @@ def test_step_trace(testdir):
|
|||
]
|
||||
|
||||
assert jsonobject == expected
|
||||
|
||||
|
||||
def test_step_trace_with_expand_option(testdir):
|
||||
"""Test step trace."""
|
||||
testdir.makefile(
|
||||
".ini",
|
||||
pytest=textwrap.dedent(
|
||||
"""
|
||||
[pytest]
|
||||
markers =
|
||||
feature-tag
|
||||
scenario-outline-passing-tag
|
||||
"""
|
||||
),
|
||||
)
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
test=textwrap.dedent(
|
||||
"""
|
||||
@feature-tag
|
||||
Feature: One scenario outline, expanded to multiple scenarios
|
||||
|
||||
@scenario-outline-passing-tag
|
||||
Scenario: Passing outline
|
||||
Given type <type> and value <value>
|
||||
|
||||
Examples: example1
|
||||
| type | value |
|
||||
| str | hello |
|
||||
| int | 42 |
|
||||
| float | 1.0 |
|
||||
"""
|
||||
),
|
||||
)
|
||||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
import pytest
|
||||
from pytest_bdd import given, scenario
|
||||
|
||||
@given('type <type> and value <value>')
|
||||
def type_type_and_value_value():
|
||||
return 'pass'
|
||||
|
||||
@scenario('test.feature', 'Passing outline')
|
||||
def test_passing_outline():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
)
|
||||
result, jsonobject = runandparse(testdir, "--cucumber-json-expanded")
|
||||
result.assert_outcomes(passed=3)
|
||||
|
||||
assert jsonobject[0]["elements"][0]["steps"][0]["name"] == "type str and value hello"
|
||||
assert jsonobject[0]["elements"][1]["steps"][0]["name"] == "type int and value 42"
|
||||
assert jsonobject[0]["elements"][2]["steps"][0]["name"] == "type float and value 1.0"
|
||||
|
||||
|
||||
def test_converters_dict_with_expand_option(testdir):
|
||||
"""Test that `--cucumber-json-expanded` works correctly when using `example_converters`."""
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
test=textwrap.dedent(
|
||||
"""
|
||||
Feature: Expanded option with example converters
|
||||
Scenario: Passing outline
|
||||
Given there is an intvalue <intvalue> and stringvalue <stringvalue> and floatvalue <floatvalue>
|
||||
|
||||
Examples: example1
|
||||
| intvalue | stringvalue | floatvalue |
|
||||
| 1 | hello | 1.0 |
|
||||
"""
|
||||
),
|
||||
)
|
||||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""
|
||||
import pytest
|
||||
from pytest_bdd import given, scenario
|
||||
|
||||
@given('there is an intvalue <intvalue> and stringvalue <stringvalue> and floatvalue <floatvalue>')
|
||||
def type_type_and_value_value():
|
||||
pass
|
||||
|
||||
@scenario(
|
||||
'test.feature',
|
||||
'Passing outline',
|
||||
example_converters={"intvalue":int, "stringvalue":str, "floatvalue":float},
|
||||
)
|
||||
def test_passing_outline():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
)
|
||||
result, jsonobject = runandparse(testdir, "--cucumber-json-expanded")
|
||||
assert result.ret == 0
|
||||
|
||||
expanded_step_name = jsonobject[0]["elements"][0]["steps"][0]["name"]
|
||||
assert expanded_step_name == "there is an intvalue 1 and stringvalue hello and floatvalue 1.0"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import textwrap
|
||||
import pytest
|
||||
|
||||
import pytest
|
||||
|
||||
FEATURE = """\
|
||||
Feature: Gherkin terminal output feature
|
||||
|
@ -204,18 +204,18 @@ def test_step_parameters_should_be_replaced_by_their_values(testdir):
|
|||
testdir.makepyfile(
|
||||
test_gherkin=textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd import given, when, scenario, then
|
||||
from pytest_bdd import given, when, scenario, then, parsers
|
||||
|
||||
@given('there are <start> cucumbers', target_fixture="start_cucumbers")
|
||||
@given(parsers.parse('there are {start} cucumbers'), target_fixture="start_cucumbers")
|
||||
def start_cucumbers(start):
|
||||
return start
|
||||
|
||||
@when('I eat <eat> cucumbers')
|
||||
@when(parsers.parse('I eat {eat} cucumbers'))
|
||||
def eat_cucumbers(start_cucumbers, eat):
|
||||
pass
|
||||
|
||||
@then('I should have <left> cucumbers')
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
@then(parsers.parse('I should have {left} cucumbers'))
|
||||
def should_have_left_cucumbers(start_cucumbers, left):
|
||||
pass
|
||||
|
||||
@scenario('test.feature', 'Scenario example 2')
|
||||
|
@ -225,7 +225,7 @@ def test_step_parameters_should_be_replaced_by_their_values(testdir):
|
|||
)
|
||||
)
|
||||
|
||||
result = testdir.runpytest("--gherkin-terminal-reporter", "--gherkin-terminal-reporter-expanded", "-vv")
|
||||
result = testdir.runpytest("--gherkin-terminal-reporter", "-vv")
|
||||
result.assert_outcomes(passed=1, failed=0)
|
||||
result.stdout.fnmatch_lines("*Scenario: Scenario example 2")
|
||||
result.stdout.fnmatch_lines("*Given there are {start} cucumbers".format(**example))
|
||||
|
|
|
@ -3,78 +3,19 @@
|
|||
|
||||
def test_background_no_strict_gherkin(testdir):
|
||||
"""Test background no strict gherkin."""
|
||||
prepare_test_dir(testdir)
|
||||
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
no_strict_gherkin_background="""
|
||||
Feature: No strict Gherkin Background support
|
||||
|
||||
Background:
|
||||
When foo has a value "bar"
|
||||
And foo is not boolean
|
||||
And foo has not a value "baz"
|
||||
|
||||
Scenario: Test background
|
||||
|
||||
""",
|
||||
)
|
||||
|
||||
result = testdir.runpytest("-k", "test_background_ok")
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
def test_scenario_no_strict_gherkin(testdir):
|
||||
"""Test scenario no strict gherkin."""
|
||||
prepare_test_dir(testdir)
|
||||
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
no_strict_gherkin_scenario="""
|
||||
Feature: No strict Gherkin Scenario support
|
||||
|
||||
Scenario: Test scenario
|
||||
When foo has a value "bar"
|
||||
And foo is not boolean
|
||||
And foo has not a value "baz"
|
||||
|
||||
""",
|
||||
)
|
||||
|
||||
result = testdir.runpytest("-k", "test_scenario_ok")
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
def prepare_test_dir(testdir):
|
||||
"""Test scenario no strict gherkin."""
|
||||
testdir.makepyfile(
|
||||
test_gherkin="""
|
||||
import pytest
|
||||
|
||||
from pytest_bdd import (
|
||||
when,
|
||||
scenario,
|
||||
from pytest_bdd import when, scenario
|
||||
|
||||
@scenario(
|
||||
"no_strict_gherkin_background.feature",
|
||||
"Test background",
|
||||
)
|
||||
def test_background():
|
||||
pass
|
||||
|
||||
def test_scenario_ok(request):
|
||||
@scenario(
|
||||
"no_strict_gherkin_scenario.feature",
|
||||
"Test scenario",
|
||||
)
|
||||
def test():
|
||||
pass
|
||||
|
||||
test(request)
|
||||
|
||||
def test_background_ok(request):
|
||||
@scenario(
|
||||
"no_strict_gherkin_background.feature",
|
||||
"Test background",
|
||||
)
|
||||
def test():
|
||||
pass
|
||||
|
||||
test(request)
|
||||
|
||||
@pytest.fixture
|
||||
def foo():
|
||||
|
@ -96,3 +37,75 @@ def prepare_test_dir(testdir):
|
|||
assert "baz" not in foo
|
||||
"""
|
||||
)
|
||||
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
no_strict_gherkin_background="""
|
||||
Feature: No strict Gherkin Background support
|
||||
|
||||
Background:
|
||||
When foo has a value "bar"
|
||||
And foo is not boolean
|
||||
And foo has not a value "baz"
|
||||
|
||||
Scenario: Test background
|
||||
|
||||
""",
|
||||
)
|
||||
|
||||
result = testdir.runpytest()
|
||||
result.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
def test_scenario_no_strict_gherkin(testdir):
|
||||
"""Test scenario no strict gherkin."""
|
||||
testdir.makepyfile(
|
||||
test_gherkin="""
|
||||
import pytest
|
||||
|
||||
from pytest_bdd import when, scenario
|
||||
|
||||
@scenario(
|
||||
"no_strict_gherkin_scenario.feature",
|
||||
"Test scenario",
|
||||
)
|
||||
def test_scenario():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def foo():
|
||||
return {}
|
||||
|
||||
@when('foo has a value "bar"')
|
||||
def bar(foo):
|
||||
foo["bar"] = "bar"
|
||||
return foo["bar"]
|
||||
|
||||
|
||||
@when('foo is not boolean')
|
||||
def not_boolean(foo):
|
||||
assert foo is not bool
|
||||
|
||||
|
||||
@when('foo has not a value "baz"')
|
||||
def has_not_baz(foo):
|
||||
assert "baz" not in foo
|
||||
"""
|
||||
)
|
||||
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
no_strict_gherkin_scenario="""
|
||||
Feature: No strict Gherkin Scenario support
|
||||
|
||||
Scenario: Test scenario
|
||||
When foo has a value "bar"
|
||||
And foo is not boolean
|
||||
And foo has not a value "baz"
|
||||
|
||||
""",
|
||||
)
|
||||
|
||||
result = testdir.runpytest()
|
||||
result.assert_outcomes(passed=1)
|
||||
|
|
|
@ -1,27 +1,32 @@
|
|||
"""Scenario Outline tests."""
|
||||
import textwrap
|
||||
|
||||
from pytest_bdd.utils import collect_dumped_objects
|
||||
from tests.utils import assert_outcomes
|
||||
|
||||
STEPS = """\
|
||||
from pytest_bdd import given, when, then
|
||||
from pytest_bdd import parsers, given, when, then
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
|
||||
@given("there are <start> cucumbers", target_fixture="start_cucumbers")
|
||||
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="start_cucumbers")
|
||||
def start_cucumbers(start):
|
||||
assert isinstance(start, int)
|
||||
return dict(start=start)
|
||||
dump_obj(start)
|
||||
return {"start": start}
|
||||
|
||||
|
||||
@when("I eat <eat> cucumbers")
|
||||
@when(parsers.parse("I eat {eat:g} cucumbers"))
|
||||
def eat_cucumbers(start_cucumbers, eat):
|
||||
assert isinstance(eat, float)
|
||||
dump_obj(eat)
|
||||
start_cucumbers["eat"] = eat
|
||||
|
||||
|
||||
@then("I should have <left> cucumbers")
|
||||
@then(parsers.parse("I should have {left} cucumbers"))
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
assert isinstance(left, str)
|
||||
dump_obj(left)
|
||||
assert start - eat == int(left)
|
||||
assert start_cucumbers["start"] == start
|
||||
assert start_cucumbers["eat"] == eat
|
||||
|
@ -54,28 +59,26 @@ def test_outlined(testdir):
|
|||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd.utils import get_parametrize_markers_args
|
||||
from pytest_bdd import scenario
|
||||
|
||||
@scenario(
|
||||
"outline.feature",
|
||||
"Outlined given, when, thens",
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_outline(request):
|
||||
assert get_parametrize_markers_args(request.node) == (
|
||||
["start", "eat", "left"],
|
||||
[
|
||||
[12, 5.0, "7"],
|
||||
[5, 4.0, "1"],
|
||||
],
|
||||
)
|
||||
pass
|
||||
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=2)
|
||||
# fmt: off
|
||||
assert collect_dumped_objects(result) == [
|
||||
12, 5.0, "7",
|
||||
5, 4.0, "1",
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
def test_wrongly_outlined(testdir):
|
||||
|
@ -227,7 +230,6 @@ def test_outlined_with_other_fixtures(testdir):
|
|||
textwrap.dedent(
|
||||
"""\
|
||||
import pytest
|
||||
from pytest_bdd.utils import get_parametrize_markers_args
|
||||
from pytest_bdd import scenario
|
||||
|
||||
|
||||
|
@ -239,7 +241,6 @@ def test_outlined_with_other_fixtures(testdir):
|
|||
@scenario(
|
||||
"outline.feature",
|
||||
"Outlined given, when, thens",
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_outline(other_fixture):
|
||||
pass
|
||||
|
@ -277,28 +278,26 @@ def test_vertical_example(testdir):
|
|||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd.utils import get_parametrize_markers_args
|
||||
from pytest_bdd import scenario
|
||||
|
||||
@scenario(
|
||||
"outline.feature",
|
||||
"Outlined with vertical example table",
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_outline(request):
|
||||
assert get_parametrize_markers_args(request.node) == (
|
||||
["start", "eat", "left"],
|
||||
[
|
||||
[12, 5.0, "7"],
|
||||
[2, 1.0, "1"],
|
||||
],
|
||||
)
|
||||
|
||||
def test_outline():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=2)
|
||||
parametrizations = collect_dumped_objects(result)
|
||||
# fmt: off
|
||||
assert parametrizations == [
|
||||
12, 5.0, "7",
|
||||
2, 1.0, "1",
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
def test_outlined_feature(testdir):
|
||||
|
@ -329,36 +328,36 @@ def test_outlined_feature(testdir):
|
|||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd.utils import get_parametrize_markers_args
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
from pytest_bdd import given, when, then, scenario, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
@scenario(
|
||||
"outline.feature",
|
||||
"Outlined given, when, thens",
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_outline(request):
|
||||
assert get_parametrize_markers_args(request.node) == (
|
||||
["start", "eat", "left"],
|
||||
[[12, 5.0, "7"], [5, 4.0, "1"]],
|
||||
["fruits"],
|
||||
[["oranges"], ["apples"]],
|
||||
)
|
||||
def test_outline():
|
||||
pass
|
||||
|
||||
@given("there are <start> <fruits>", target_fixture="start_fruits")
|
||||
@given(parsers.parse("there are {start:d} {fruits}"), target_fixture="start_fruits")
|
||||
def start_fruits(start, fruits):
|
||||
dump_obj(start, fruits)
|
||||
|
||||
assert isinstance(start, int)
|
||||
return {fruits: dict(start=start)}
|
||||
|
||||
|
||||
@when("I eat <eat> <fruits>")
|
||||
@when(parsers.parse("I eat {eat:g} {fruits}"))
|
||||
def eat_fruits(start_fruits, eat, fruits):
|
||||
dump_obj(eat, fruits)
|
||||
|
||||
assert isinstance(eat, float)
|
||||
start_fruits[fruits]["eat"] = eat
|
||||
|
||||
|
||||
@then("I should have <left> <fruits>")
|
||||
@then(parsers.parse("I should have {left} {fruits}"))
|
||||
def should_have_left_fruits(start_fruits, start, eat, left, fruits):
|
||||
dump_obj(left, fruits)
|
||||
|
||||
assert isinstance(left, str)
|
||||
assert start - eat == int(left)
|
||||
assert start_fruits[fruits]["start"] == start
|
||||
|
@ -367,8 +366,17 @@ def test_outlined_feature(testdir):
|
|||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=4)
|
||||
parametrizations = collect_dumped_objects(result)
|
||||
# fmt: off
|
||||
assert parametrizations == [
|
||||
12, "oranges", 5.0, "oranges", "7", "oranges",
|
||||
12, "apples", 5.0, "apples", "7", "apples",
|
||||
5, "oranges", 4.0, "oranges", "1", "oranges",
|
||||
5, "apples", 4.0, "apples", "1", "apples",
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
def test_outline_with_escaped_pipes(testdir):
|
||||
|
@ -380,18 +388,18 @@ def test_outline_with_escaped_pipes(testdir):
|
|||
Feature: Outline With Special characters
|
||||
|
||||
Scenario Outline: Outline with escaped pipe character
|
||||
Given We have strings <string1> and <string2>
|
||||
Then <string2> should be the base64 encoding of <string1>
|
||||
# Just print the string so that we can assert later what it was by reading the output
|
||||
Given I print the <string>
|
||||
|
||||
Examples:
|
||||
| string1 | string2 |
|
||||
| bork | Ym9yaw== |
|
||||
| \|bork | fGJvcms= |
|
||||
| bork \| | Ym9yayB8 |
|
||||
| bork\|\|bork | Ym9ya3x8Ym9yaw== |
|
||||
| \| | fA== |
|
||||
| bork \\ | Ym9yayAgICAgIFxc |
|
||||
| bork \\\| | Ym9yayAgICBcXHw= |
|
||||
| string |
|
||||
| bork |
|
||||
| \|bork |
|
||||
| bork \| |
|
||||
| bork\|\|bork |
|
||||
| \| |
|
||||
| bork \\ |
|
||||
| bork \\\| |
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
@ -399,10 +407,8 @@ def test_outline_with_escaped_pipes(testdir):
|
|||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
import base64
|
||||
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.utils import get_parametrize_markers_args
|
||||
from pytest_bdd import scenario, given, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
|
||||
@scenario("outline.feature", "Outline with escaped pipe character")
|
||||
|
@ -410,17 +416,20 @@ def test_outline_with_escaped_pipes(testdir):
|
|||
pass
|
||||
|
||||
|
||||
@given("We have strings <string1> and <string2>")
|
||||
def we_have_strings_string1_and_string2(string1, string2):
|
||||
pass
|
||||
|
||||
|
||||
@then("<string2> should be the base64 encoding of <string1>")
|
||||
def string2_should_be_base64_encoding_of_string1(string2, string1):
|
||||
assert string1.encode() == base64.b64decode(string2.encode())
|
||||
|
||||
@given(parsers.parse("I print the {string}"))
|
||||
def i_print_the_string(string):
|
||||
dump_obj(string)
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=7)
|
||||
assert collect_dumped_objects(result) == [
|
||||
r"bork",
|
||||
r"|bork",
|
||||
r"bork |",
|
||||
r"bork||bork",
|
||||
r"|",
|
||||
r"bork \\",
|
||||
r"bork \\|",
|
||||
]
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
"""Scenario Outline with empty example values tests."""
|
||||
import textwrap
|
||||
|
||||
from pytest_bdd.utils import collect_dumped_objects
|
||||
|
||||
STEPS = """\
|
||||
from pytest_bdd import given, when, then
|
||||
from pytest_bdd import given, when, then, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
# Using `parsers.re` so that we can match empty values
|
||||
|
||||
@given("there are <start> cucumbers")
|
||||
@given(parsers.re("there are (?P<start>.*?) cucumbers"))
|
||||
def start_cucumbers(start):
|
||||
pass
|
||||
dump_obj(start)
|
||||
|
||||
|
||||
@when("I eat <eat> cucumbers")
|
||||
@when(parsers.re("I eat (?P<eat>.*?) cucumbers"))
|
||||
def eat_cucumbers(eat):
|
||||
pass
|
||||
dump_obj(eat)
|
||||
|
||||
|
||||
@then("I should have <left> cucumbers")
|
||||
@then(parsers.re("I should have (?P<left>.*?) cucumbers"))
|
||||
def should_have_left_cucumbers(left):
|
||||
pass
|
||||
dump_obj(left)
|
||||
|
||||
"""
|
||||
|
||||
|
@ -45,18 +48,19 @@ def test_scenario_with_empty_example_values(testdir):
|
|||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd.utils import get_parametrize_markers_args
|
||||
from pytest_bdd.utils import dump_obj
|
||||
from pytest_bdd import scenario
|
||||
import json
|
||||
|
||||
@scenario("outline.feature", "Outlined with empty example values")
|
||||
def test_outline(request):
|
||||
assert get_parametrize_markers_args(request.node) == ([u"start", u"eat", u"left"], [["#", "", ""]])
|
||||
|
||||
def test_outline():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
assert collect_dumped_objects(result) == ["#", "", ""]
|
||||
|
||||
|
||||
def test_scenario_with_empty_example_values_vertical(testdir):
|
||||
|
@ -82,15 +86,15 @@ def test_scenario_with_empty_example_values_vertical(testdir):
|
|||
testdir.makepyfile(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
from pytest_bdd.utils import get_parametrize_markers_args
|
||||
from pytest_bdd.utils import dump_obj
|
||||
from pytest_bdd import scenario
|
||||
|
||||
@scenario("outline.feature", "Outlined with empty example values vertical")
|
||||
def test_outline(request):
|
||||
assert get_parametrize_markers_args(request.node) == ([u"start", u"eat", u"left"], [["#", "", ""]])
|
||||
|
||||
def test_outline():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=1)
|
||||
assert collect_dumped_objects(result) == ["#", "", ""]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import textwrap
|
||||
|
||||
from pytest_bdd.utils import collect_dumped_objects
|
||||
|
||||
|
||||
def test_parametrized(testdir):
|
||||
"""Test parametrized scenario."""
|
||||
|
@ -9,9 +11,9 @@ def test_parametrized(testdir):
|
|||
"""\
|
||||
Feature: Parametrized scenario
|
||||
Scenario: Parametrized given, when, thens
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
Given there are {start} cucumbers
|
||||
When I eat {eat} cucumbers
|
||||
Then I should have {left} cucumbers
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
@ -20,14 +22,10 @@ def test_parametrized(testdir):
|
|||
textwrap.dedent(
|
||||
"""\
|
||||
import pytest
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
from pytest_bdd import given, when, then, scenario, parsers
|
||||
from pytest_bdd.utils import dump_obj
|
||||
|
||||
|
||||
@pytest.mark.parametrize(["start", "eat", "left"], [(12, 5, 7)])
|
||||
@scenario("parametrized.feature", "Parametrized given, when, thens")
|
||||
def test_parametrized(request, start, eat, left):
|
||||
pass
|
||||
|
||||
@pytest.fixture(params=[1, 2])
|
||||
def foo_bar(request):
|
||||
return "bar" * request.param
|
||||
|
@ -35,21 +33,31 @@ def test_parametrized(testdir):
|
|||
|
||||
@pytest.mark.parametrize(["start", "eat", "left"], [(12, 5, 7)])
|
||||
@scenario("parametrized.feature", "Parametrized given, when, thens")
|
||||
def test_parametrized(request, start, eat, left):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(["start", "eat", "left"], [(2, 1, 1)])
|
||||
@scenario("parametrized.feature", "Parametrized given, when, thens")
|
||||
def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar):
|
||||
pass
|
||||
|
||||
@given("there are <start> cucumbers", target_fixture="start_cucumbers")
|
||||
|
||||
@given(parsers.parse("there are {start} cucumbers"), target_fixture="start_cucumbers")
|
||||
def start_cucumbers(start):
|
||||
dump_obj(start)
|
||||
return dict(start=start)
|
||||
|
||||
|
||||
@when("I eat <eat> cucumbers")
|
||||
@when(parsers.parse("I eat {eat} cucumbers"))
|
||||
def eat_cucumbers(start_cucumbers, start, eat):
|
||||
dump_obj(eat)
|
||||
start_cucumbers["eat"] = eat
|
||||
|
||||
|
||||
@then("I should have <left> cucumbers")
|
||||
@then(parsers.parse("I should have {left} cucumbers"))
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
dump_obj(left)
|
||||
assert start - eat == left
|
||||
assert start_cucumbers["start"] == start
|
||||
assert start_cucumbers["eat"] == eat
|
||||
|
@ -57,5 +65,15 @@ def test_parametrized(testdir):
|
|||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpytest()
|
||||
result = testdir.runpytest("-s")
|
||||
result.assert_outcomes(passed=3)
|
||||
|
||||
parametrizations = collect_dumped_objects(result)
|
||||
# fmt: off
|
||||
assert parametrizations == [
|
||||
12, 5, 7,
|
||||
# The second test uses is duplicated because of the `foo_bar` indirect fixture
|
||||
2, 1, 1,
|
||||
2, 1, 1,
|
||||
]
|
||||
# fmt: on
|
||||
|
|
|
@ -62,7 +62,7 @@ def test_step_trace(testdir):
|
|||
textwrap.dedent(
|
||||
"""
|
||||
import pytest
|
||||
from pytest_bdd import given, when, then, scenarios
|
||||
from pytest_bdd import given, when, then, scenarios, parsers
|
||||
|
||||
@given('a passing step')
|
||||
def a_passing_step():
|
||||
|
@ -76,26 +76,27 @@ def test_step_trace(testdir):
|
|||
def a_failing_step():
|
||||
raise Exception('Error')
|
||||
|
||||
@given('there are <start> cucumbers', target_fixture="start_cucumbers")
|
||||
@given(parsers.parse('there are {start:d} cucumbers'), target_fixture="start_cucumbers")
|
||||
def start_cucumbers(start):
|
||||
assert isinstance(start, int)
|
||||
return dict(start=start)
|
||||
return {"start": start}
|
||||
|
||||
|
||||
@when('I eat <eat> cucumbers')
|
||||
@when(parsers.parse('I eat {eat:g} cucumbers'))
|
||||
def eat_cucumbers(start_cucumbers, eat):
|
||||
assert isinstance(eat, float)
|
||||
start_cucumbers['eat'] = eat
|
||||
|
||||
|
||||
@then('I should have <left> cucumbers')
|
||||
@then(parsers.parse('I should have {left} cucumbers'))
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
assert isinstance(left, str)
|
||||
assert start - eat == int(left)
|
||||
assert start_cucumbers['start'] == start
|
||||
assert start_cucumbers['eat'] == eat
|
||||
|
||||
scenarios('test.feature', example_converters=dict(start=int, eat=float, left=str))
|
||||
|
||||
scenarios('test.feature')
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
@ -132,8 +133,6 @@ def test_step_trace(testdir):
|
|||
},
|
||||
],
|
||||
"tags": ["scenario-passing-tag"],
|
||||
"examples": [],
|
||||
"example_kwargs": {},
|
||||
}
|
||||
|
||||
assert report == expected
|
||||
|
@ -169,12 +168,10 @@ def test_step_trace(testdir):
|
|||
},
|
||||
],
|
||||
"tags": ["scenario-failing-tag"],
|
||||
"examples": [],
|
||||
"example_kwargs": {},
|
||||
}
|
||||
assert report == expected
|
||||
|
||||
report = result.matchreport("test_outlined[12-5.0-7]", when="call").scenario
|
||||
report = result.matchreport("test_outlined[12-5-7]", when="call").scenario
|
||||
expected = {
|
||||
"feature": {
|
||||
"description": "",
|
||||
|
@ -192,7 +189,7 @@ def test_step_trace(testdir):
|
|||
"failed": False,
|
||||
"keyword": "Given",
|
||||
"line_number": 15,
|
||||
"name": "there are <start> cucumbers",
|
||||
"name": "there are 12 cucumbers",
|
||||
"type": "given",
|
||||
},
|
||||
{
|
||||
|
@ -200,7 +197,7 @@ def test_step_trace(testdir):
|
|||
"failed": False,
|
||||
"keyword": "When",
|
||||
"line_number": 16,
|
||||
"name": "I eat <eat> cucumbers",
|
||||
"name": "I eat 5 cucumbers",
|
||||
"type": "when",
|
||||
},
|
||||
{
|
||||
|
@ -208,24 +205,15 @@ def test_step_trace(testdir):
|
|||
"failed": False,
|
||||
"keyword": "Then",
|
||||
"line_number": 17,
|
||||
"name": "I should have <left> cucumbers",
|
||||
"name": "I should have 7 cucumbers",
|
||||
"type": "then",
|
||||
},
|
||||
],
|
||||
"tags": [],
|
||||
"examples": [
|
||||
{
|
||||
"line_number": 19,
|
||||
"name": None,
|
||||
"row_index": 0,
|
||||
"rows": [["start", "eat", "left"], [[12, 5.0, "7"], [5, 4.0, "1"]]],
|
||||
}
|
||||
],
|
||||
"example_kwargs": {"eat": "5.0", "left": "7", "start": "12"},
|
||||
}
|
||||
assert report == expected
|
||||
|
||||
report = result.matchreport("test_outlined[5-4.0-1]", when="call").scenario
|
||||
report = result.matchreport("test_outlined[5-4-1]", when="call").scenario
|
||||
expected = {
|
||||
"feature": {
|
||||
"description": "",
|
||||
|
@ -243,7 +231,7 @@ def test_step_trace(testdir):
|
|||
"failed": False,
|
||||
"keyword": "Given",
|
||||
"line_number": 15,
|
||||
"name": "there are <start> cucumbers",
|
||||
"name": "there are 5 cucumbers",
|
||||
"type": "given",
|
||||
},
|
||||
{
|
||||
|
@ -251,7 +239,7 @@ def test_step_trace(testdir):
|
|||
"failed": False,
|
||||
"keyword": "When",
|
||||
"line_number": 16,
|
||||
"name": "I eat <eat> cucumbers",
|
||||
"name": "I eat 4 cucumbers",
|
||||
"type": "when",
|
||||
},
|
||||
{
|
||||
|
@ -259,31 +247,22 @@ def test_step_trace(testdir):
|
|||
"failed": False,
|
||||
"keyword": "Then",
|
||||
"line_number": 17,
|
||||
"name": "I should have <left> cucumbers",
|
||||
"name": "I should have 1 cucumbers",
|
||||
"type": "then",
|
||||
},
|
||||
],
|
||||
"tags": [],
|
||||
"examples": [
|
||||
{
|
||||
"line_number": 19,
|
||||
"name": None,
|
||||
"row_index": 1,
|
||||
"rows": [["start", "eat", "left"], [[12, 5.0, "7"], [5, 4.0, "1"]]],
|
||||
}
|
||||
],
|
||||
"example_kwargs": {"eat": "4.0", "left": "1", "start": "5"},
|
||||
}
|
||||
assert report == expected
|
||||
|
||||
|
||||
def test_complex_types(testdir):
|
||||
def test_complex_types(testdir, pytestconfig):
|
||||
"""Test serialization of the complex types."""
|
||||
try:
|
||||
import execnet.gateway_base
|
||||
except ImportError:
|
||||
if not pytestconfig.pluginmanager.has_plugin("xdist"):
|
||||
pytest.skip("Execnet not installed")
|
||||
|
||||
import execnet.gateway_base
|
||||
|
||||
testdir.makefile(
|
||||
".feature",
|
||||
test=textwrap.dedent(
|
||||
|
@ -303,9 +282,9 @@ def test_complex_types(testdir):
|
|||
textwrap.dedent(
|
||||
"""
|
||||
import pytest
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
from pytest_bdd import given, when, then, scenario, parsers
|
||||
|
||||
class Point(object):
|
||||
class Point:
|
||||
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
|
@ -318,14 +297,18 @@ def test_complex_types(testdir):
|
|||
class Alien(object):
|
||||
pass
|
||||
|
||||
@given('there is a coordinate <point>')
|
||||
def point(point):
|
||||
@given(
|
||||
parsers.parse('there is a coordinate {point}'),
|
||||
target_fixture="point",
|
||||
converters={"point": Point.parse},
|
||||
)
|
||||
def given_there_is_a_point(point):
|
||||
assert isinstance(point, Point)
|
||||
return point
|
||||
|
||||
|
||||
@pytest.mark.parametrize('alien', [Alien()])
|
||||
@scenario('test.feature', 'Complex', example_converters=dict(point=Point.parse))
|
||||
@scenario('test.feature', 'Complex')
|
||||
def test_complex(alien):
|
||||
pass
|
||||
|
||||
|
@ -333,6 +316,7 @@ def test_complex_types(testdir):
|
|||
)
|
||||
)
|
||||
result = testdir.inline_run("-vvl")
|
||||
report = result.matchreport("test_complex[point0-alien0]", when="call")
|
||||
report = result.matchreport("test_complex[10,20-alien0]", when="call")
|
||||
assert report.passed
|
||||
assert execnet.gateway_base.dumps(report.item)
|
||||
assert execnet.gateway_base.dumps(report.scenario)
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
"""Test when and then steps are callables."""
|
||||
|
||||
import pytest
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_when_then(testdir):
|
||||
"""Test when and then steps are callable functions.
|
||||
|
|
Loading…
Reference in New Issue