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:
Alessio Bogon 2021-09-23 21:07:15 +02:00 committed by GitHub
parent c6b7134e59
commit 379cb4b47c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 541 additions and 608 deletions

View File

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: 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: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -2,9 +2,14 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.6b0 rev: 21.9b0
hooks: hooks:
- id: black - 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 - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1 rev: v4.0.1
hooks: hooks:
@ -13,7 +18,7 @@ repos:
- id: check-yaml - id: check-yaml
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.19.4 rev: v2.26.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py36-plus] args: [--py36-plus]

View File

@ -3,6 +3,12 @@ Changelog
Unreleased 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 4.1.0
----------- -----------

View File

@ -109,15 +109,8 @@ test_publish_article.py:
Scenario decorator 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, Functions decorated with the `scenario` decorator behave like a normal test function,
and they will be executed after all scenario steps. 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 .. code-block:: python
@ -129,6 +122,9 @@ call other functions and make assertions:
assert article.title in browser.html 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 Step aliases
------------ ------------
@ -239,7 +235,7 @@ Example:
.. code-block:: gherkin .. code-block:: gherkin
Feature: Step arguments Feature: Step arguments
Scenario: Arguments for given, when, thens Scenario: Arguments for given, when, then
Given there are 5 cucumbers Given there are 5 cucumbers
When I eat 3 cucumbers When I eat 3 cucumbers
@ -256,7 +252,7 @@ The code will look like:
from pytest_bdd import scenario, given, when, then, parsers 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(): def test_arguments():
pass 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): def __init__(self, name, **kwargs):
"""Compile regex.""" """Compile regex."""
super(re, self).__init__(name) super().__init__(name)
self.regex = re.compile(re.sub("%(.+)%", "(?P<\1>.+)", self.name), **kwargs) self.regex = re.compile(re.sub("%(.+)%", "(?P<\1>.+)", self.name), **kwargs)
def parse_arguments(self, name): 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 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. * 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 import re
from pytest_bdd import given, then, scenario from pytest_bdd import given, then, scenario, parsers
@scenario( @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' 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 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. 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 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 `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. exactly as it's described in be behave_ docs.
@ -517,7 +513,7 @@ Example:
.. code-block:: gherkin .. code-block:: gherkin
Feature: Scenario outlines Feature: Scenario outlines
Scenario Outline: Outlined given, when, thens Scenario Outline: Outlined given, when, then
Given there are <start> cucumbers Given there are <start> cucumbers
When I eat <eat> cucumbers When I eat <eat> cucumbers
Then I should have <left> 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 .. code-block:: gherkin
Feature: Scenario outlines Feature: Scenario outlines
Scenario Outline: Outlined given, when, thens Scenario Outline: Outlined given, when, then
Given there are <start> cucumbers Given there are <start> cucumbers
When I eat <eat> cucumbers When I eat <eat> cucumbers
Then I should have <left> cucumbers Then I should have <left> cucumbers
@ -549,31 +545,30 @@ The code will look like:
.. code-block:: python .. code-block:: python
from pytest_bdd import given, when, then, scenario from pytest_bdd import given, when, then, scenario, parsers
@scenario( @scenario(
"outline.feature", "outline.feature",
"Outlined given, when, thens", "Outlined given, when, then",
example_converters=dict(start=int, eat=float, left=str)
) )
def test_outlined(): def test_outlined():
pass 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): def start_cucumbers(start):
assert isinstance(start, int) assert isinstance(start, int)
return dict(start=start) return dict(start=start)
@when("I eat <eat> cucumbers") @when(parsers.parse("I eat {eat:g} cucumbers"))
def eat_cucumbers(start_cucumbers, eat): def eat_cucumbers(start_cucumbers, eat):
assert isinstance(eat, float) assert isinstance(eat, float)
start_cucumbers["eat"] = 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): def should_have_left_cucumbers(start_cucumbers, start, eat, left):
assert isinstance(left, str) assert isinstance(left, str)
assert start - eat == int(left) assert start - eat == int(left)
@ -654,7 +649,7 @@ The code will look like:
.. code-block:: python .. code-block:: python
import pytest 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 # Here we use pytest to parametrize the test with the parameters table
@ -664,7 +659,7 @@ The code will look like:
) )
@scenario( @scenario(
"parametrized.feature", "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 # 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). # 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.""" """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): def start_cucumbers(start):
return dict(start=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): def eat_cucumbers(start_cucumbers, start, eat):
start_cucumbers["eat"] = 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): def should_have_left_cucumbers(start_cucumbers, start, eat, left):
assert start - eat == left assert start - eat == left
assert start_cucumbers["start"] == start assert start_cucumbers["start"] == start
@ -694,7 +689,7 @@ With a parametrized.feature file:
.. code-block:: gherkin .. code-block:: gherkin
Feature: parametrized Feature: parametrized
Scenario: Parametrized given, when, thens Scenario: Parametrized given, when, then
Given there are <start> cucumbers Given there are <start> cucumbers
When I eat <eat> cucumbers When I eat <eat> cucumbers
Then I should have <left> 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" 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 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`. 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 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 `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 alphanumberic, etc. That way you can safely use tags for tests filtering. 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 You can customize how tags are converted to pytest marks by implementing the
``pytest_bdd_apply_tag`` hook and returning ``True`` from it: ``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) marker(function)
return True return True
else: else:
# Fall back to pytest-bdd's default behavior # Fall back to the default behavior of pytest-bdd
return None return None
Test setup Test setup
@ -978,23 +973,7 @@ test_common.py:
pass pass
There are no definitions of the steps in the test file. They were There are no definitions of the steps in the test file. They were
collected from the parent conftests. collected from the parent conftest.py.
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
Default steps 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 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: This will make your life much easier when defining multiple scenarios in a test file. For example:
test_publish_article.py: 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 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>`_ `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 which can be used for, for example, by `this <https://plugins.jenkins.io/cucumber-testresult-plugin/>`_ Jenkins
plugin plugin.
To have an output in json format: To have an output in json format:
@ -1128,11 +1107,6 @@ To have an output in json format:
pytest --cucumberjson=<path to json report> pytest --cucumberjson=<path to json report>
This will output an expanded (meaning scenario outlines will be expanded to several scenarios) cucumber format. 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 To enable gherkin-formatted output on terminal, use
@ -1141,14 +1115,6 @@ To enable gherkin-formatted output on terminal, use
pytest --gherkin-terminal-reporter pytest --gherkin-terminal-reporter
Terminal reporter supports expanded format as well
::
pytest --gherkin-terminal-reporter-expanded
Test code generation helpers 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. 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 from 3.x.x:
Migration of your tests from versions 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. Step validation handlers for the hook ``pytest_bdd_step_validation_error`` should be removed.
License License
------- -------

View File

@ -5,3 +5,8 @@ build-backend = "setuptools.build_meta"
[tool.black] [tool.black]
line-length = 120 line-length = 120
target-version = ['py36', 'py37', 'py38'] target-version = ['py36', 'py37', 'py38']
[tool.isort]
profile = "black"
line_length = 120
multi_line_output = 3

View File

@ -1,7 +1,7 @@
"""pytest-bdd public API.""" """pytest-bdd public API."""
from pytest_bdd.steps import given, when, then
from pytest_bdd.scenario import scenario, scenarios from pytest_bdd.scenario import scenario, scenarios
from pytest_bdd.steps import given, then, when
__version__ = "4.1.0" __version__ = "4.1.0"

View File

@ -19,21 +19,12 @@ def add_options(parser):
help="create cucumber json style report file at given path.", 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): def configure(config):
cucumber_json_path = config.option.cucumber_json_path cucumber_json_path = config.option.cucumber_json_path
# prevent opening json log on worker nodes (xdist) # prevent opening json log on worker nodes (xdist)
if cucumber_json_path and not hasattr(config, "workerinput"): 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) config.pluginmanager.register(config._bddcucumberjson)
@ -48,11 +39,10 @@ class LogBDDCucumberJSON:
"""Logging plugin for cucumber like json output.""" """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)) logfile = os.path.expanduser(os.path.expandvars(logfile))
self.logfile = os.path.normpath(os.path.abspath(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile))
self.features = {} self.features = {}
self.expand = expand
def append(self, obj): def append(self, obj):
self.features[-1].append(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"]] 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): def pytest_runtest_logreport(self, report):
try: try:
scenario = report.scenario scenario = report.scenario
@ -122,12 +95,6 @@ class LogBDDCucumberJSON:
scenario["failed"] = True scenario["failed"] = True
error_message = 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 { return {

View File

@ -20,20 +20,21 @@ Syntax example:
And the article should be published # Note: will query the database And the article should be published # Note: will query the database
:note: The "#" symbol is used for comments. :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. one line.
""" """
import os.path import os.path
import typing
import glob2 import glob2
from .parser import parse_feature from .parser import Feature, parse_feature
# Global features dictionary # 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. """Get a feature by the filename.
:param str base_path: Base feature directory. :param str base_path: Base feature directory.
@ -55,7 +56,7 @@ def get_feature(base_path, filename, encoding="utf-8"):
return feature return feature
def get_features(paths, **kwargs): def get_features(paths: typing.List[str], **kwargs) -> typing.List[Feature]:
"""Get features for given paths. """Get features for given paths.
:param list paths: `list` of paths (file or dirs) :param list paths: `list` of paths (file or dirs)

View File

@ -1,9 +1,5 @@
import re
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from .parser import STEP_PARAM_RE
def add_options(parser): def add_options(parser):
group = parser.getgroup("terminal reporting", "reporting", after="general") group = parser.getgroup("terminal reporting", "reporting", after="general")
@ -12,14 +8,7 @@ def add_options(parser):
action="store_true", action="store_true",
dest="gherkin_terminal_reporter", dest="gherkin_terminal_reporter",
default=False, default=False,
help=("enable gherkin output"), 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",
) )
@ -93,24 +82,9 @@ class GherkinTerminalReporter(TerminalReporter):
self._tw.write(report.scenario["name"], **scenario_markup) self._tw.write(report.scenario["name"], **scenario_markup)
self._tw.write("\n") self._tw.write("\n")
for step in report.scenario["steps"]: for step in report.scenario["steps"]:
if self.config.option.expand: self._tw.write(f" {step['keyword']} {step['name']}\n", **scenario_markup)
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(" " + word, **word_markup) self._tw.write(" " + word, **word_markup)
self._tw.write("\n\n") self._tw.write("\n\n")
else: else:
return TerminalReporter.pytest_runtest_logreport(self, rep) return TerminalReporter.pytest_runtest_logreport(self, rep)
self.stats.setdefault(cat, []).append(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

View File

@ -1,12 +1,13 @@
import io
import os.path import os.path
import re import re
import textwrap import textwrap
import typing
from collections import OrderedDict from collections import OrderedDict
from . import types, exceptions from . import exceptions, types
SPLIT_LINE_RE = re.compile(r"(?<!\\)\|") SPLIT_LINE_RE = re.compile(r"(?<!\\)\|")
STEP_PARAM_RE = re.compile(r"<(.+?)>")
COMMENT_RE = re.compile(r"(^|(?<=\s))#") COMMENT_RE = re.compile(r"(^|(?<=\s))#")
STEP_PREFIXES = [ STEP_PREFIXES = [
("Feature: ", types.FEATURE), ("Feature: ", types.FEATURE),
@ -73,7 +74,7 @@ def get_step_type(line):
return _type 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. """Parse the feature file.
:param str basedir: Feature files base directory. :param str basedir: Feature files base directory.
@ -93,10 +94,10 @@ def parse_feature(basedir, filename, encoding="utf-8"):
background=None, background=None,
description="", description="",
) )
scenario = None scenario: typing.Optional[ScenarioTemplate] = None
mode = None mode = None
prev_mode = None prev_mode = None
description = [] description: typing.List[str] = []
step = None step = None
multiline_step = False multiline_step = False
prev_line = None prev_line = None
@ -149,7 +150,9 @@ def parse_feature(basedir, filename, encoding="utf-8"):
keyword, parsed_line = parse_line(clean_line) keyword, parsed_line = parse_line(clean_line)
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]: if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
tags = get_tags(prev_line) 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: elif mode == types.BACKGROUND:
feature.background = Background(feature=feature, line_number=line_number) feature.background = Background(feature=feature, line_number=line_number)
elif mode == types.EXAMPLES: elif mode == types.EXAMPLES:
@ -199,42 +202,35 @@ class Feature:
"""Feature.""" """Feature."""
def __init__(self, scenarios, filename, rel_filename, name, tags, examples, background, line_number, description): 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.rel_filename = rel_filename
self.filename = filename self.filename = filename
self.name = name
self.tags = tags self.tags = tags
self.examples = examples self.examples = examples
self.name = name self.name = name
self.line_number = line_number self.line_number = line_number
self.tags = tags
self.scenarios = scenarios
self.description = description self.description = description
self.background = background 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): def __init__(self, feature: Feature, name: str, line_number: int, tags=None):
"""Scenario constructor. """
:param pytest_bdd.parser.Feature feature: Feature.
:param str name: Scenario name. :param str name: Scenario name.
:param int line_number: Scenario line number. :param int line_number: Scenario line number.
:param dict example_converters: Example table parameter converters.
:param set tags: Set of tags. :param set tags: Set of tags.
""" """
self.feature = feature self.feature = feature
self.name = name self.name = name
self._steps = [] self._steps: typing.List[Step] = []
self.examples = Examples() self.examples = Examples()
self.line_number = line_number self.line_number = line_number
self.example_converters = example_converters
self.tags = tags or set() self.tags = tags or set()
self.failed = False
self.test_function = None
def add_step(self, step): def add_step(self, step):
"""Add step to the scenario. """Add step to the scenario.
@ -246,41 +242,29 @@ class Scenario:
@property @property
def steps(self): 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. def render(self, context: typing.Mapping[str, typing.Any]) -> "Scenario":
""" steps = [
result = [] Step(
if self.feature.background: name=templated_step.render(context),
result.extend(self.feature.background.steps) type=templated_step.type,
result.extend(self._steps) indent=templated_step.indent,
return result line_number=templated_step.line_number,
keyword=templated_step.keyword,
@property )
def params(self): for templated_step in self.steps
"""Get parameter names. ]
return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags)
: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 validate(self): def validate(self):
"""Validate the scenario. """Validate the scenario.
:raises ScenarioValidationError: when scenario is not valid :raises ScenarioValidationError: when scenario is not valid
""" """
params = self.params params = frozenset(sum((list(step.params) for step in self.steps), []))
example_params = self.get_example_params() example_params = set(self.examples.example_params + self.feature.examples.example_params)
if params and example_params and params != example_params: if params and example_params and params != example_params:
raise exceptions.ScenarioExamplesNotValidError( raise exceptions.ScenarioExamplesNotValidError(
"""Scenario "{}" in the feature "{}" has not valid examples. """ """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: class Step:
"""Step.""" """Step."""
@ -352,6 +356,13 @@ class Step:
"""Get step params.""" """Get step params."""
return tuple(frozenset(STEP_PARAM_RE.findall(self.name))) 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: class Background:
@ -412,11 +423,7 @@ class Examples:
self.example_params.append(param) self.example_params.append(param)
self.vertical_examples.append(values) self.vertical_examples.append(values)
def get_params(self, converters, builtin=False): def as_contexts(self) -> typing.Iterable[typing.Dict[str, typing.Any]]:
"""Get scenario pytest parametrization table.
:param converters: `dict` of converter functions to convert parameter values
"""
param_count = len(self.example_params) param_count = len(self.example_params)
if self.vertical_examples and not self.examples: if self.vertical_examples and not self.examples:
for value_index in range(len(self.vertical_examples[0])): 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]) example.append(self.vertical_examples[param_index][value_index])
self.examples.append(example) self.examples.append(example)
if self.examples: if not self.examples:
params = [] return
for example in self.examples:
example = list(example) header, rows = self.example_params, self.examples
for index, param in enumerate(self.example_params):
raw_value = example[index] for row in rows:
if converters and param in converters: assert len(header) == len(row)
value = converters[param](raw_value)
if not builtin or value.__class__.__module__ in {"__builtin__", "builtins"}: yield dict(zip(header, row))
example[index] = value
params.append(example)
return [self.example_params, params]
else:
return []
def __bool__(self): def __bool__(self):
"""Bool comparison.""" """Bool comparison."""
@ -455,6 +457,3 @@ def get_tags(line):
if not line or not line.strip().startswith("@"): if not line or not line.strip().startswith("@"):
return set() return set()
return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1} return {tag.lstrip("@") for tag in line.strip().split(" @") if len(tag) > 1}
STEP_PARAM_RE = re.compile(r"\<(.+?)\>")

View File

@ -2,11 +2,7 @@
import pytest import pytest
from . import cucumber_json from . import cucumber_json, generation, gherkin_terminal_reporter, given, reporting, then, when
from . import generation
from . import gherkin_terminal_reporter
from . import given, when, then
from . import reporting
from .utils import CONFIG_STACK from .utils import CONFIG_STACK
@ -25,6 +21,20 @@ def trace():
pytest.set_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): def pytest_addoption(parser):
"""Add pytest-bdd options.""" """Add pytest-bdd options."""
add_bdd_ini(parser) add_bdd_ini(parser)

View File

@ -1,16 +1,14 @@
"""Reporting functionality. """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. that enriches the pytest test reporting.
""" """
import time import time
from .utils import get_parametrize_markers_args
class StepReport: class StepReport:
"""Step excecution report.""" """Step execution report."""
failed = False failed = False
stopped = None stopped = None
@ -21,12 +19,12 @@ class StepReport:
:param pytest_bdd.parser.Step step: Step. :param pytest_bdd.parser.Step step: Step.
""" """
self.step = step self.step = step
self.started = time.time() self.started = time.perf_counter()
def serialize(self): 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 :rtype: dict
""" """
return { return {
@ -41,16 +39,16 @@ class StepReport:
def finalize(self, failed): def finalize(self, failed):
"""Stop collecting information and finalize the report. """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 self.failed = failed
@property @property
def duration(self): def duration(self):
"""Step excecution duration. """Step execution duration.
:return: Step excecution duration. :return: Step execution duration.
:rtype: float :rtype: float
""" """
if self.stopped is None: if self.stopped is None:
@ -70,21 +68,6 @@ class ScenarioReport:
""" """
self.scenario = scenario self.scenario = scenario
self.step_reports = [] 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 @property
def current_step_report(self): def current_step_report(self):
@ -104,7 +87,7 @@ class ScenarioReport:
self.step_reports.append(step_report) self.step_reports.append(step_report)
def serialize(self): 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. :return: Serialized report.
:rtype: dict :rtype: dict
@ -112,7 +95,6 @@ class ScenarioReport:
scenario = self.scenario scenario = self.scenario
feature = scenario.feature feature = scenario.feature
params = sum(scenario.get_params(builtin=True), []) if scenario.examples else None
return { return {
"steps": [step_report.serialize() for step_report in self.step_reports], "steps": [step_report.serialize() for step_report in self.step_reports],
"name": scenario.name, "name": scenario.name,
@ -126,17 +108,6 @@ class ScenarioReport:
"description": feature.description, "description": feature.description,
"tags": sorted(feature.tags), "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): def fail(self):

View File

@ -13,6 +13,7 @@ test_publish_article = scenario(
import collections import collections
import os import os
import re import re
import typing
import pytest import pytest
from _pytest.fixtures import FixtureLookupError 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 .steps import get_step_fixture_name, inject_fixture
from .utils import CONFIG_STACK, get_args, get_caller_module_locals, get_caller_module_path 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") PYTHON_REPLACE_REGEX = re.compile(r"\W")
ALPHA_REGEX = re.compile(r"^\d+_*") ALPHA_REGEX = re.compile(r"^\d+_*")
@ -38,6 +44,8 @@ def find_argumented_step_fixture_name(name, type_, fixturemanager, request=None)
if not match: if not match:
continue continue
# TODO: maybe `converters` should be part of the SterParser.__init__(),
# and used by StepParser.parse_arguments() method
converters = getattr(fixturedef.func, "converters", {}) converters = getattr(fixturedef.func, "converters", {})
for arg, value in parser.parse_arguments(name).items(): for arg, value in parser.parse_arguments(name).items():
if arg in converters: if arg in converters:
@ -113,7 +121,7 @@ def _execute_step_function(request, scenario, step, step_func):
raise raise
def _execute_scenario(feature, scenario, request): def _execute_scenario(feature: "Feature", scenario: "Scenario", request):
"""Execute the scenario. """Execute the scenario.
:param feature: Feature. :param feature: Feature.
@ -141,7 +149,9 @@ def _execute_scenario(feature, scenario, request):
FakeRequest = collections.namedtuple("FakeRequest", ["module"]) 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 # HACK: Ideally we would use `def decorator(fn)`, but we want to return a custom exception
# when the decorator is misused. # when the decorator is misused.
# Pytest inspect the signature to determine the required fixtures, and in that case it would look # 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 [fn] = args
args = get_args(fn) 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) # We need to tell pytest that the original function requires its fixtures,
def scenario_wrapper(request): # 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) _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(): example_parametrizations = collect_example_parametrizations(templated_scenario)
if param_set: if example_parametrizations is not None:
scenario_wrapper = pytest.mark.parametrize(*param_set)(scenario_wrapper) # Parametrize the scenario outlines
for tag in scenario.tags.union(feature.tags): 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 = CONFIG_STACK[-1]
config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper) config.hook.pytest_bdd_apply_tag(tag=tag, function=scenario_wrapper)
scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}" scenario_wrapper.__doc__ = f"{feature_name}: {scenario_name}"
scenario_wrapper.__scenario__ = scenario scenario_wrapper.__scenario__ = templated_scenario
scenario.test_function = scenario_wrapper
return scenario_wrapper return scenario_wrapper
return decorator 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. """Scenario decorator.
:param str feature_name: Feature file name. Absolute or relative to the configured feature base path. :param str feature_name: Feature file name. Absolute or relative to the configured feature base path.
:param str scenario_name: Scenario name. :param str scenario_name: Scenario name.
:param str encoding: Feature file encoding. :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) 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.' f'Scenario "{scenario_name}" in feature "{feature_name}" in {feature.filename} is not found.'
) )
scenario.example_converters = example_converters
# Validate the scenario # Validate the scenario
scenario.validate() scenario.validate()
return _get_scenario_decorator( 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
) )

View File

@ -8,7 +8,7 @@ import glob2
from .generation import generate_code, parse_feature_files 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): def migrate_tests(args):

View File

@ -36,11 +36,10 @@ def given_beautiful_article(article):
""" """
import pytest import pytest
from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureDef
from .types import GIVEN, WHEN, THEN
from .parsers import get_parser from .parsers import get_parser
from .types import GIVEN, THEN, WHEN
from .utils import get_caller_module_locals from .utils import get_caller_module_locals

View File

@ -1,9 +1,14 @@
"""Various utility functions.""" """Various utility functions."""
import base64
from inspect import getframeinfo import pickle
from inspect import signature as _signature import re
import typing
from inspect import getframeinfo, signature
from sys import _getframe from sys import _getframe
if typing.TYPE_CHECKING:
from _pytest.pytester import RunResult
CONFIG_STACK = [] CONFIG_STACK = []
@ -15,14 +20,10 @@ def get_args(func):
:return: A list of argument names. :return: A list of argument names.
:rtype: list :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] 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): def get_caller_module_locals(depth=2):
"""Get the caller module locals dictionary. """Get the caller module locals dictionary.
@ -40,3 +41,26 @@ def get_caller_module_path(depth=2):
""" """
frame = _getframe(depth) frame = _getframe(depth)
return getframeinfo(frame, context=0).filename 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]

View File

@ -2,7 +2,6 @@
import textwrap import textwrap
FEATURE = """\ FEATURE = """\
Feature: Background support Feature: Background support

View File

@ -71,7 +71,7 @@ def test_step_trace(testdir):
textwrap.dedent( textwrap.dedent(
""" """
import pytest import pytest
from pytest_bdd import given, when, scenario from pytest_bdd import given, when, scenario, parsers
@given('a passing step') @given('a passing step')
def a_passing_step(): def a_passing_step():
@ -85,7 +85,7 @@ def test_step_trace(testdir):
def a_failing_step(): def a_failing_step():
raise Exception('Error') raise Exception('Error')
@given('type <type> and value <value>') @given(parsers.parse('type {type} and value {value}'))
def type_type_and_value_value(): def type_type_and_value_value():
return 'pass' return 'pass'
@ -104,6 +104,8 @@ def test_step_trace(testdir):
) )
) )
result, jsonobject = runandparse(testdir) result, jsonobject = runandparse(testdir)
result.assert_outcomes(passed=4, failed=1)
assert result.ret assert result.ret
expected = [ expected = [
{ {
@ -169,7 +171,7 @@ def test_step_trace(testdir):
"match": {"location": ""}, "match": {"location": ""},
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
"keyword": "Given", "keyword": "Given",
"name": "type <type> and value <value>", "name": "type str and value hello",
} }
], ],
"line": 15, "line": 15,
@ -187,7 +189,7 @@ def test_step_trace(testdir):
"match": {"location": ""}, "match": {"location": ""},
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
"keyword": "Given", "keyword": "Given",
"name": "type <type> and value <value>", "name": "type int and value 42",
} }
], ],
"line": 15, "line": 15,
@ -205,7 +207,7 @@ def test_step_trace(testdir):
"match": {"location": ""}, "match": {"location": ""},
"result": {"status": "passed", "duration": OfType(int)}, "result": {"status": "passed", "duration": OfType(int)},
"keyword": "Given", "keyword": "Given",
"name": "type <type> and value <value>", "name": "type float and value 1.0",
} }
], ],
"line": 15, "line": 15,
@ -224,102 +226,3 @@ def test_step_trace(testdir):
] ]
assert jsonobject == expected 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"

View File

@ -1,6 +1,6 @@
import textwrap import textwrap
import pytest
import pytest
FEATURE = """\ FEATURE = """\
Feature: Gherkin terminal output feature Feature: Gherkin terminal output feature
@ -204,18 +204,18 @@ def test_step_parameters_should_be_replaced_by_their_values(testdir):
testdir.makepyfile( testdir.makepyfile(
test_gherkin=textwrap.dedent( 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): def start_cucumbers(start):
return start return start
@when('I eat <eat> cucumbers') @when(parsers.parse('I eat {eat} cucumbers'))
def eat_cucumbers(start_cucumbers, eat): def eat_cucumbers(start_cucumbers, eat):
pass pass
@then('I should have <left> cucumbers') @then(parsers.parse('I should have {left} cucumbers'))
def should_have_left_cucumbers(start_cucumbers, start, eat, left): def should_have_left_cucumbers(start_cucumbers, left):
pass pass
@scenario('test.feature', 'Scenario example 2') @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.assert_outcomes(passed=1, failed=0)
result.stdout.fnmatch_lines("*Scenario: Scenario example 2") result.stdout.fnmatch_lines("*Scenario: Scenario example 2")
result.stdout.fnmatch_lines("*Given there are {start} cucumbers".format(**example)) result.stdout.fnmatch_lines("*Given there are {start} cucumbers".format(**example))

View File

@ -3,78 +3,19 @@
def test_background_no_strict_gherkin(testdir): def test_background_no_strict_gherkin(testdir):
"""Test background no strict gherkin.""" """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( testdir.makepyfile(
test_gherkin=""" test_gherkin="""
import pytest import pytest
from pytest_bdd import ( from pytest_bdd import when, scenario
when,
scenario,
)
def test_scenario_ok(request):
@scenario(
"no_strict_gherkin_scenario.feature",
"Test scenario",
)
def test():
pass
test(request)
def test_background_ok(request):
@scenario( @scenario(
"no_strict_gherkin_background.feature", "no_strict_gherkin_background.feature",
"Test background", "Test background",
) )
def test(): def test_background():
pass pass
test(request)
@pytest.fixture @pytest.fixture
def foo(): def foo():
@ -96,3 +37,75 @@ def prepare_test_dir(testdir):
assert "baz" not in foo 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)

View File

@ -1,27 +1,32 @@
"""Scenario Outline tests.""" """Scenario Outline tests."""
import textwrap import textwrap
from pytest_bdd.utils import collect_dumped_objects
from tests.utils import assert_outcomes from tests.utils import assert_outcomes
STEPS = """\ 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): def start_cucumbers(start):
assert isinstance(start, int) 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): def eat_cucumbers(start_cucumbers, eat):
assert isinstance(eat, float) assert isinstance(eat, float)
dump_obj(eat)
start_cucumbers["eat"] = 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): def should_have_left_cucumbers(start_cucumbers, start, eat, left):
assert isinstance(left, str) assert isinstance(left, str)
dump_obj(left)
assert start - eat == int(left) assert start - eat == int(left)
assert start_cucumbers["start"] == start assert start_cucumbers["start"] == start
assert start_cucumbers["eat"] == eat assert start_cucumbers["eat"] == eat
@ -54,28 +59,26 @@ def test_outlined(testdir):
testdir.makepyfile( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
"""\ """\
from pytest_bdd.utils import get_parametrize_markers_args
from pytest_bdd import scenario from pytest_bdd import scenario
@scenario( @scenario(
"outline.feature", "outline.feature",
"Outlined given, when, thens", "Outlined given, when, thens",
example_converters=dict(start=int, eat=float, left=str)
) )
def test_outline(request): def test_outline(request):
assert get_parametrize_markers_args(request.node) == ( pass
["start", "eat", "left"],
[
[12, 5.0, "7"],
[5, 4.0, "1"],
],
)
""" """
) )
) )
result = testdir.runpytest() result = testdir.runpytest("-s")
result.assert_outcomes(passed=2) 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): def test_wrongly_outlined(testdir):
@ -227,7 +230,6 @@ def test_outlined_with_other_fixtures(testdir):
textwrap.dedent( textwrap.dedent(
"""\ """\
import pytest import pytest
from pytest_bdd.utils import get_parametrize_markers_args
from pytest_bdd import scenario from pytest_bdd import scenario
@ -239,7 +241,6 @@ def test_outlined_with_other_fixtures(testdir):
@scenario( @scenario(
"outline.feature", "outline.feature",
"Outlined given, when, thens", "Outlined given, when, thens",
example_converters=dict(start=int, eat=float, left=str)
) )
def test_outline(other_fixture): def test_outline(other_fixture):
pass pass
@ -277,28 +278,26 @@ def test_vertical_example(testdir):
testdir.makepyfile( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
"""\ """\
from pytest_bdd.utils import get_parametrize_markers_args
from pytest_bdd import scenario from pytest_bdd import scenario
@scenario( @scenario(
"outline.feature", "outline.feature",
"Outlined with vertical example table", "Outlined with vertical example table",
example_converters=dict(start=int, eat=float, left=str)
) )
def test_outline(request): def test_outline():
assert get_parametrize_markers_args(request.node) == ( pass
["start", "eat", "left"],
[
[12, 5.0, "7"],
[2, 1.0, "1"],
],
)
""" """
) )
) )
result = testdir.runpytest() result = testdir.runpytest("-s")
result.assert_outcomes(passed=2) 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): def test_outlined_feature(testdir):
@ -329,36 +328,36 @@ def test_outlined_feature(testdir):
testdir.makepyfile( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
"""\ """\
from pytest_bdd.utils import get_parametrize_markers_args from pytest_bdd import given, when, then, scenario, parsers
from pytest_bdd import given, when, then, scenario from pytest_bdd.utils import dump_obj
@scenario( @scenario(
"outline.feature", "outline.feature",
"Outlined given, when, thens", "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): def start_fruits(start, fruits):
dump_obj(start, fruits)
assert isinstance(start, int) assert isinstance(start, int)
return {fruits: dict(start=start)} 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): def eat_fruits(start_fruits, eat, fruits):
dump_obj(eat, fruits)
assert isinstance(eat, float) assert isinstance(eat, float)
start_fruits[fruits]["eat"] = eat 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): def should_have_left_fruits(start_fruits, start, eat, left, fruits):
dump_obj(left, fruits)
assert isinstance(left, str) assert isinstance(left, str)
assert start - eat == int(left) assert start - eat == int(left)
assert start_fruits[fruits]["start"] == start 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) 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): def test_outline_with_escaped_pipes(testdir):
@ -380,18 +388,18 @@ def test_outline_with_escaped_pipes(testdir):
Feature: Outline With Special characters Feature: Outline With Special characters
Scenario Outline: Outline with escaped pipe character Scenario Outline: Outline with escaped pipe character
Given We have strings <string1> and <string2> # Just print the string so that we can assert later what it was by reading the output
Then <string2> should be the base64 encoding of <string1> Given I print the <string>
Examples: Examples:
| string1 | string2 | | string |
| bork | Ym9yaw== | | bork |
| \|bork | fGJvcms= | | \|bork |
| bork \| | Ym9yayB8 | | bork \| |
| bork\|\|bork | Ym9ya3x8Ym9yaw== | | bork\|\|bork |
| \| | fA== | | \| |
| bork \\ | Ym9yayAgICAgIFxc | | bork \\ |
| bork \\\| | Ym9yayAgICBcXHw= | | bork \\\| |
""" """
), ),
) )
@ -399,10 +407,8 @@ def test_outline_with_escaped_pipes(testdir):
testdir.makepyfile( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
"""\ """\
import base64 from pytest_bdd import scenario, given, parsers
from pytest_bdd.utils import dump_obj
from pytest_bdd import scenario, given, when, then
from pytest_bdd.utils import get_parametrize_markers_args
@scenario("outline.feature", "Outline with escaped pipe character") @scenario("outline.feature", "Outline with escaped pipe character")
@ -410,17 +416,20 @@ def test_outline_with_escaped_pipes(testdir):
pass pass
@given("We have strings <string1> and <string2>") @given(parsers.parse("I print the {string}"))
def we_have_strings_string1_and_string2(string1, string2): def i_print_the_string(string):
pass dump_obj(string)
@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())
""" """
) )
) )
result = testdir.runpytest() result = testdir.runpytest("-s")
result.assert_outcomes(passed=7) result.assert_outcomes(passed=7)
assert collect_dumped_objects(result) == [
r"bork",
r"|bork",
r"bork |",
r"bork||bork",
r"|",
r"bork \\",
r"bork \\|",
]

View File

@ -1,24 +1,27 @@
"""Scenario Outline with empty example values tests.""" """Scenario Outline with empty example values tests."""
import textwrap import textwrap
from pytest_bdd.utils import collect_dumped_objects
STEPS = """\ 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): 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): 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): def should_have_left_cucumbers(left):
pass dump_obj(left)
""" """
@ -45,18 +48,19 @@ def test_scenario_with_empty_example_values(testdir):
testdir.makepyfile( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
"""\ """\
from pytest_bdd.utils import get_parametrize_markers_args from pytest_bdd.utils import dump_obj
from pytest_bdd import scenario from pytest_bdd import scenario
import json
@scenario("outline.feature", "Outlined with empty example values") @scenario("outline.feature", "Outlined with empty example values")
def test_outline(request): def test_outline():
assert get_parametrize_markers_args(request.node) == ([u"start", u"eat", u"left"], [["#", "", ""]]) pass
""" """
) )
) )
result = testdir.runpytest() result = testdir.runpytest("-s")
result.assert_outcomes(passed=1) result.assert_outcomes(passed=1)
assert collect_dumped_objects(result) == ["#", "", ""]
def test_scenario_with_empty_example_values_vertical(testdir): def test_scenario_with_empty_example_values_vertical(testdir):
@ -82,15 +86,15 @@ def test_scenario_with_empty_example_values_vertical(testdir):
testdir.makepyfile( testdir.makepyfile(
textwrap.dedent( textwrap.dedent(
"""\ """\
from pytest_bdd.utils import get_parametrize_markers_args from pytest_bdd.utils import dump_obj
from pytest_bdd import scenario from pytest_bdd import scenario
@scenario("outline.feature", "Outlined with empty example values vertical") @scenario("outline.feature", "Outlined with empty example values vertical")
def test_outline(request): def test_outline():
assert get_parametrize_markers_args(request.node) == ([u"start", u"eat", u"left"], [["#", "", ""]]) pass
""" """
) )
) )
result = testdir.runpytest() result = testdir.runpytest("-s")
result.assert_outcomes(passed=1) result.assert_outcomes(passed=1)
assert collect_dumped_objects(result) == ["#", "", ""]

View File

@ -1,5 +1,7 @@
import textwrap import textwrap
from pytest_bdd.utils import collect_dumped_objects
def test_parametrized(testdir): def test_parametrized(testdir):
"""Test parametrized scenario.""" """Test parametrized scenario."""
@ -9,9 +11,9 @@ def test_parametrized(testdir):
"""\ """\
Feature: Parametrized scenario Feature: Parametrized scenario
Scenario: Parametrized given, when, thens Scenario: Parametrized given, when, thens
Given there are <start> cucumbers Given there are {start} cucumbers
When I eat <eat> cucumbers When I eat {eat} cucumbers
Then I should have <left> cucumbers Then I should have {left} cucumbers
""" """
), ),
) )
@ -20,14 +22,10 @@ def test_parametrized(testdir):
textwrap.dedent( textwrap.dedent(
"""\ """\
import pytest 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]) @pytest.fixture(params=[1, 2])
def foo_bar(request): def foo_bar(request):
return "bar" * request.param return "bar" * request.param
@ -35,21 +33,31 @@ def test_parametrized(testdir):
@pytest.mark.parametrize(["start", "eat", "left"], [(12, 5, 7)]) @pytest.mark.parametrize(["start", "eat", "left"], [(12, 5, 7)])
@scenario("parametrized.feature", "Parametrized given, when, thens") @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): def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar):
pass 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): def start_cucumbers(start):
dump_obj(start)
return dict(start=start) return dict(start=start)
@when("I eat <eat> cucumbers") @when(parsers.parse("I eat {eat} cucumbers"))
def eat_cucumbers(start_cucumbers, start, eat): def eat_cucumbers(start_cucumbers, start, eat):
dump_obj(eat)
start_cucumbers["eat"] = 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): def should_have_left_cucumbers(start_cucumbers, start, eat, left):
dump_obj(left)
assert start - eat == left assert start - eat == left
assert start_cucumbers["start"] == start assert start_cucumbers["start"] == start
assert start_cucumbers["eat"] == eat 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) 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

View File

@ -62,7 +62,7 @@ def test_step_trace(testdir):
textwrap.dedent( textwrap.dedent(
""" """
import pytest import pytest
from pytest_bdd import given, when, then, scenarios from pytest_bdd import given, when, then, scenarios, parsers
@given('a passing step') @given('a passing step')
def a_passing_step(): def a_passing_step():
@ -76,26 +76,27 @@ def test_step_trace(testdir):
def a_failing_step(): def a_failing_step():
raise Exception('Error') 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): def start_cucumbers(start):
assert isinstance(start, int) 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): def eat_cucumbers(start_cucumbers, eat):
assert isinstance(eat, float) assert isinstance(eat, float)
start_cucumbers['eat'] = 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): def should_have_left_cucumbers(start_cucumbers, start, eat, left):
assert isinstance(left, str) assert isinstance(left, str)
assert start - eat == int(left) assert start - eat == int(left)
assert start_cucumbers['start'] == start assert start_cucumbers['start'] == start
assert start_cucumbers['eat'] == eat 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"], "tags": ["scenario-passing-tag"],
"examples": [],
"example_kwargs": {},
} }
assert report == expected assert report == expected
@ -169,12 +168,10 @@ def test_step_trace(testdir):
}, },
], ],
"tags": ["scenario-failing-tag"], "tags": ["scenario-failing-tag"],
"examples": [],
"example_kwargs": {},
} }
assert report == expected 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 = { expected = {
"feature": { "feature": {
"description": "", "description": "",
@ -192,7 +189,7 @@ def test_step_trace(testdir):
"failed": False, "failed": False,
"keyword": "Given", "keyword": "Given",
"line_number": 15, "line_number": 15,
"name": "there are <start> cucumbers", "name": "there are 12 cucumbers",
"type": "given", "type": "given",
}, },
{ {
@ -200,7 +197,7 @@ def test_step_trace(testdir):
"failed": False, "failed": False,
"keyword": "When", "keyword": "When",
"line_number": 16, "line_number": 16,
"name": "I eat <eat> cucumbers", "name": "I eat 5 cucumbers",
"type": "when", "type": "when",
}, },
{ {
@ -208,24 +205,15 @@ def test_step_trace(testdir):
"failed": False, "failed": False,
"keyword": "Then", "keyword": "Then",
"line_number": 17, "line_number": 17,
"name": "I should have <left> cucumbers", "name": "I should have 7 cucumbers",
"type": "then", "type": "then",
}, },
], ],
"tags": [], "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 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 = { expected = {
"feature": { "feature": {
"description": "", "description": "",
@ -243,7 +231,7 @@ def test_step_trace(testdir):
"failed": False, "failed": False,
"keyword": "Given", "keyword": "Given",
"line_number": 15, "line_number": 15,
"name": "there are <start> cucumbers", "name": "there are 5 cucumbers",
"type": "given", "type": "given",
}, },
{ {
@ -251,7 +239,7 @@ def test_step_trace(testdir):
"failed": False, "failed": False,
"keyword": "When", "keyword": "When",
"line_number": 16, "line_number": 16,
"name": "I eat <eat> cucumbers", "name": "I eat 4 cucumbers",
"type": "when", "type": "when",
}, },
{ {
@ -259,31 +247,22 @@ def test_step_trace(testdir):
"failed": False, "failed": False,
"keyword": "Then", "keyword": "Then",
"line_number": 17, "line_number": 17,
"name": "I should have <left> cucumbers", "name": "I should have 1 cucumbers",
"type": "then", "type": "then",
}, },
], ],
"tags": [], "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 assert report == expected
def test_complex_types(testdir): def test_complex_types(testdir, pytestconfig):
"""Test serialization of the complex types.""" """Test serialization of the complex types."""
try: if not pytestconfig.pluginmanager.has_plugin("xdist"):
import execnet.gateway_base
except ImportError:
pytest.skip("Execnet not installed") pytest.skip("Execnet not installed")
import execnet.gateway_base
testdir.makefile( testdir.makefile(
".feature", ".feature",
test=textwrap.dedent( test=textwrap.dedent(
@ -303,9 +282,9 @@ def test_complex_types(testdir):
textwrap.dedent( textwrap.dedent(
""" """
import pytest 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): def __init__(self, x, y):
self.x = x self.x = x
@ -318,14 +297,18 @@ def test_complex_types(testdir):
class Alien(object): class Alien(object):
pass pass
@given('there is a coordinate <point>') @given(
def point(point): 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) assert isinstance(point, Point)
return point return point
@pytest.mark.parametrize('alien', [Alien()]) @pytest.mark.parametrize('alien', [Alien()])
@scenario('test.feature', 'Complex', example_converters=dict(point=Point.parse)) @scenario('test.feature', 'Complex')
def test_complex(alien): def test_complex(alien):
pass pass
@ -333,6 +316,7 @@ def test_complex_types(testdir):
) )
) )
result = testdir.inline_run("-vvl") 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.item)
assert execnet.gateway_base.dumps(report.scenario) assert execnet.gateway_base.dumps(report.scenario)

View File

@ -1,8 +1,9 @@
"""Test when and then steps are callables.""" """Test when and then steps are callables."""
import pytest
import textwrap import textwrap
import pytest
def test_when_then(testdir): def test_when_then(testdir):
"""Test when and then steps are callable functions. """Test when and then steps are callable functions.