Skip to content

Commit

Permalink
Merge branch 'master' into pre-commit-ci-update-config
Browse files Browse the repository at this point in the history
  • Loading branch information
youtux committed Jul 4, 2022
2 parents c99d95c + 2ae7d33 commit 2411ac2
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 88 deletions.
19 changes: 10 additions & 9 deletions CHANGES.rst
Expand Up @@ -6,15 +6,16 @@ Unreleased

This release introduces breaking changes in order to be more in line with the official gherkin specification.

- Cleanup of the documentation and tests related to parametrization (elchupanebrej)
- Removed feature level examples for the gherkin compatibility (olegpidsadnyi)
- Removed vertical examples for the gherkin compatibility (olegpidsadnyi)
- Step arguments are no longer fixtures (olegpidsadnyi)
- Drop support of python 3.6, pytest 4 (elchupanebrej)
- Step definitions can have "yield" statements again (4.0 release broke it). They will be executed as normal fixtures: code after the yield is executed during teardown of the test. (youtux)
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi)
- Add type decorations
- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods.
- Cleanup of the documentation and tests related to parametrization (elchupanebrej) https://github.com/pytest-dev/pytest-bdd/pull/469
- Removed feature level examples for the gherkin compatibility (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/490
- Removed vertical examples for the gherkin compatibility (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/492
- Step arguments are no longer fixtures (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/493
- Drop support of python 3.6, pytest 4 (elchupanebrej) https://github.com/pytest-dev/pytest-bdd/pull/495 https://github.com/pytest-dev/pytest-bdd/pull/504
- Step definitions can have "yield" statements again (4.0 release broke it). They will be executed as normal fixtures: code after the yield is executed during teardown of the test. (youtux) https://github.com/pytest-dev/pytest-bdd/pull/503
- Scenario outlines unused example parameter validation is removed (olegpidsadnyi) https://github.com/pytest-dev/pytest-bdd/pull/499
- Add type annotations (youtux) https://github.com/pytest-dev/pytest-bdd/pull/505
- ``pytest_bdd.parsers.StepParser`` now is an Abstract Base Class. Subclasses must make sure to implement the abstract methods. (youtux) https://github.com/pytest-dev/pytest-bdd/pull/505
- Angular brackets in step definitions are only parsed in "Scenario Outline" (previously they were parsed also in normal "Scenario"s) (youtux) https://github.com/pytest-dev/pytest-bdd/pull/524.



Expand Down
122 changes: 63 additions & 59 deletions README.rst
Expand Up @@ -248,28 +248,25 @@ The code will look like:

.. code-block:: python
import re
from pytest_bdd import scenario, given, when, then, parsers
from pytest_bdd import scenarios, given, when, then, parsers
@scenario("arguments.feature", "Arguments for given, when, then")
def test_arguments():
pass
scenarios("arguments.feature")
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
return dict(start=start, eat=0)
return {"start": start, "eat": 0}
@when(parsers.parse("I eat {eat:d} cucumbers"))
def eat_cucumbers(cucumbers, eat):
start_cucumbers["eat"] += eat
cucumbers["eat"] += eat
@then(parsers.parse("I should have {left:d} cucumbers"))
def should_have_left_cucumbers(cucumbers, left):
assert cucumbers['start'] - cucumbers['eat'] == left
assert cucumbers["start"] - cucumbers["eat"] == left
Example code also shows possibility to pass argument converters which may be useful if you need to postprocess step
arguments after the parser.
Expand Down Expand Up @@ -390,7 +387,7 @@ Multiline steps
---------------

As Gherkin, pytest-bdd supports multiline steps
(aka `PyStrings <http://behat.org/en/v3.0/user_guide/writing_scenarios.html#pystrings>`_).
(a.k.a. `Doc Strings <https://cucumber.io/docs/gherkin/reference/#doc-strings>`_).
But in much cleaner and powerful way:

.. code-block:: gherkin
Expand All @@ -416,41 +413,27 @@ step arguments and capture lines after first line (or some subset of them) into

.. code-block:: python
import re
from pytest_bdd import given, then, scenario, parsers
@scenario(
'multiline.feature',
'Multiline step using sub indentation',
)
def test_multiline():
pass
scenarios("multiline.feature")
@given(parsers.parse("I have a step with:\n{text}"), target_fixture="i_have_text")
def i_have_text(text):
return text
@given(parsers.parse("I have a step with:\n{content}"), target_fixture="text")
def given_text(content):
return content
@then("the text should be parsed with correct indentation")
def text_should_be_correct(i_have_text, text):
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 `given` step (`i_have_text`) argument with the same name (`text`). This possibility is described in
the `Step arguments are fixtures as well!`_ section.
def text_should_be_correct(text):
assert text == "Some\nExtra\nLines"
Scenarios shortcut
------------------

If you have relatively large set of feature files, it's boring to manually bind scenarios to the tests using the
scenario decorator. Of course with the manual approach you get all the power to be able to additionally parametrize
the test, give the test function a nice name, document it, etc, but in the majority of the cases you don't need that.
Instead you want to bind `all` scenarios found in the `feature` folder(s) recursively automatically.
For this - there's a `scenarios` helper.
If you have relatively large set of feature files, it's boring to manually bind scenarios to the tests using the scenario decorator. Of course with the manual approach you get all the power to be able to additionally parametrize the test, give the test function a nice name, document it, etc, but in the majority of the cases you don't need that.
Instead, you want to bind all the scenarios found in the ``features`` folder(s) recursively automatically, by using the ``scenarios`` helper.

.. code-block:: python
Expand All @@ -459,7 +442,7 @@ For this - there's a `scenarios` helper.
# assume 'features' subfolder is in this file's directory
scenarios('features')
That's all you need to do to bind all scenarios found in the `features` folder!
That's all you need to do to bind all scenarios found in the ``features`` folder!
Note that you can pass multiple paths, and those paths can be either feature files or feature folders.


Expand All @@ -471,7 +454,7 @@ Note that you can pass multiple paths, and those paths can be either feature fil
scenarios('features', 'other_features/some.feature', 'some_other_features')
But what if you need to manually bind certain scenario, leaving others to be automatically bound?
Just write your scenario in a `normal` way, but ensure you do it `BEFORE` the call of `scenarios` helper.
Just write your scenario in a "normal" way, but ensure you do it **before** the call of ``scenarios`` helper.


.. code-block:: python
Expand All @@ -485,22 +468,20 @@ Just write your scenario in a `normal` way, but ensure you do it `BEFORE` the ca
# assume 'features' subfolder is in this file's directory
scenarios('features')
In the example above `test_something` scenario binding will be kept manual, other scenarios found in the `features`
folder will be bound automatically.
In the example above, the ``test_something`` scenario binding will be kept manual, other scenarios found in the ``features`` folder will be bound automatically.


Scenario outlines
-----------------

Scenarios can be parametrized to cover few cases. In Gherkin the variable
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 the behave_ docs.
Scenarios can be parametrized to cover few cases. These are called `Scenario Outlines <https://cucumber.io/docs/gherkin/reference/#scenario-outline>`_ in Gherkin, and the variable templates are written using angular brackets (e.g. ``<var_name>``).

Example:

.. code-block:: gherkin
# content of scenario_outlines.feature
Feature: Scenario outlines
Scenario Outline: Outlined given, when, then
Given there are <start> cucumbers
Expand All @@ -511,6 +492,28 @@ Example:
| start | eat | left |
| 12 | 5 | 7 |
.. code-block:: python
from pytest_bdd import scenarios, given, when, then, parsers
scenarios("scenario_outlines.feature")
@given(parsers.parse("there are {start:d} cucumbers"), target_fixture="cucumbers")
def given_cucumbers(start):
return {"start": start, "eat": 0}
@when(parsers.parse("I eat {eat:d} cucumbers"))
def eat_cucumbers(cucumbers, eat):
cucumbers["eat"] += eat
@then(parsers.parse("I should have {left:d} cucumbers"))
def should_have_left_cucumbers(cucumbers, left):
assert cucumbers["start"] - cucumbers["eat"] == left
Organizing your scenarios
-------------------------
Expand Down Expand Up @@ -561,9 +564,9 @@ completely different:


For picking up tests to run we can use
`tests selection <http://pytest.org/latest/usage.html#specifying-tests-selecting-tests>`_ technique. The problem is that
`tests selection <https://pytest.org/en/7.1.x/how-to/usage.html#specifying-which-tests-to-run>`_ technique. The problem is that
you have to know how your tests are organized, knowing only the feature files organization is not enough.
`cucumber tags <https://github.com/cucumber/cucumber/wiki/Tags>`_ introduce standard way of categorizing your features
Cucumber uses `tags <https://cucumber.io/docs/cucumber/api/#tags>`_ as a way of categorizing your features
and scenarios, which pytest-bdd supports. For example, we could have:

.. code-block:: gherkin
Expand All @@ -575,19 +578,15 @@ and scenarios, which pytest-bdd supports. For example, we could have:
Scenario: Successful login
pytest-bdd uses `pytest markers <http://pytest.org/latest/mark.html#mark>`_ as a `storage` of the tags for the given
pytest-bdd uses `pytest markers <http://pytest.org/latest/mark.html>`_ as a `storage` of the tags for the given
scenario test, so we can use standard test selection:

.. code-block:: bash
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
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-compatible variable
names, eg starts with a non-number, underscore alphanumeric, etc. That way you can safely use tags for tests filtering.
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-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:
Expand Down Expand Up @@ -686,7 +685,7 @@ Backgrounds

It's often the case that to cover certain feature, you'll need multiple scenarios. And it's logical that the
setup for those scenarios will have some common parts (if not equal). For this, there are `backgrounds`.
pytest-bdd implements `Gherkin backgrounds <http://behat.org/en/v3.0/user_guide/writing_scenarios.html#backgrounds>`_ for
pytest-bdd implements `Gherkin backgrounds <https://cucumber.io/docs/gherkin/reference/#background>`_ for
features.

.. code-block:: gherkin
Expand All @@ -711,8 +710,8 @@ features.
In this example, all steps from the background will be executed before all the scenario's own given
steps, adding possibility to prepare some common setup for multiple scenarios in a single feature.
About background best practices, please read
`here <https://github.com/cucumber/cucumber/wiki/Background#good-practices-for-using-background>`_.
About background best practices, please read Gherkin's
`Tips for using Background <https://cucumber.io/docs/gherkin/reference/#tips-for-using-background>`_.

.. NOTE:: There is only step "Given" should be used in "Background" section,
steps "When" and "Then" are prohibited, because their purpose are
Expand Down Expand Up @@ -868,15 +867,15 @@ This will make your life much easier when defining multiple scenarios in a test
pass
You can learn more about `functools.partial <http://docs.python.org/2/library/functools.html#functools.partial>`_
You can learn more about `functools.partial <https://docs.python.org/3/library/functools.html#functools.partial>`_
in the Python docs.


Hooks
-----

pytest-bdd exposes several `pytest hooks <http://pytest.org/latest/plugins.html#well-specified-hooks>`_
which might be helpful building useful reporting, visualization, etc on top of it:
pytest-bdd exposes several `pytest hooks <https://docs.pytest.org/en/7.1.x/reference/reference.html#hooks>`_
which might be helpful building useful reporting, visualization, etc. on top of it:

* pytest_bdd_before_scenario(request, feature, scenario) - Called before scenario is executed

Expand All @@ -903,7 +902,7 @@ Browser testing

Tools recommended to use for browser testing:

* pytest-splinter_ - pytest `splinter <http://splinter.cobrateam.info/>`_ integration for the real browser testing
* pytest-splinter_ - pytest `splinter <https://splinter.readthedocs.io/>`_ integration for the real browser testing


Reporting
Expand Down Expand Up @@ -1002,8 +1001,7 @@ gherkin reference. This means deprecation of some non-standard features that wer

Removal of the feature examples
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The example tables on the feature level are no longer supported. The tests should be parametrized with the example tables
on the scenario level.
The example tables on the feature level are no longer supported. If you had examples on the feature level, you should copy them to each individual scenario.


Removal of the vertical examples
Expand All @@ -1018,6 +1016,12 @@ Step parsed arguments conflicted with the fixtures. Now they no longer define fi
If the fixture has to be defined by the step the target_fixture param should be used.


Variable templates in steps are only parsed for Scenario Outlines
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In previous versions of pytest, steps containing ``<variable>`` would be parsed both by ``Scenario`` and ``Scenario Outline``.
Now they are only parsed within a ``Scenario Outline``.


.. _Migration from 4.x.x:

Migration of your tests from versions 4.x.x
Expand Down Expand Up @@ -1106,6 +1110,6 @@ Step validation handlers for the hook ``pytest_bdd_step_validation_error`` shoul
License
-------

This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_.
This software is licensed under the `MIT License <https://opensource.org/licenses/MIT>`_.

© 2013-2014 Oleg Pidsadnyi, Anatoly Bubenkov and others
© 2013 Oleg Pidsadnyi, Anatoly Bubenkov and others
42 changes: 28 additions & 14 deletions pytest_bdd/parser.py
Expand Up @@ -153,11 +153,17 @@ def parse_feature(basedir: str, filename: str, encoding: str = "utf-8") -> Featu

# Remove Feature, Given, When, Then, And
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 = ScenarioTemplate(
feature=feature, name=parsed_line, line_number=line_number, tags=tags
scenario = ScenarioTemplate(
feature=feature,
name=parsed_line,
line_number=line_number,
tags=tags,
templated=mode == types.SCENARIO_OUTLINE,
)
feature.scenarios[parsed_line] = scenario
elif mode == types.BACKGROUND:
feature.background = Background(feature=feature, line_number=line_number)
elif mode == types.EXAMPLES:
Expand Down Expand Up @@ -210,7 +216,7 @@ class ScenarioTemplate:
Created when parsing the feature file, it will then be combined with the examples to create a Scenario."""

def __init__(self, feature: Feature, name: str, line_number: int, tags=None) -> None:
def __init__(self, feature: Feature, name: str, line_number: int, templated: bool, tags=None) -> None:
"""
:param str name: Scenario name.
Expand All @@ -223,6 +229,7 @@ def __init__(self, feature: Feature, name: str, line_number: int, tags=None) ->
self.examples = Examples()
self.line_number = line_number
self.tags = tags or set()
self.templated = templated

def add_step(self, step: Step) -> None:
"""Add step to the scenario.
Expand All @@ -238,31 +245,38 @@ def steps(self) -> list[Step]:
return (background.steps if background else []) + self._steps

def render(self, context: Mapping[str, 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
]
background_steps = self.feature.background.steps if self.feature.background else []
if not self.templated:
scenario_steps = self._steps
else:
scenario_steps = [
Step(
name=step.render(context),
type=step.type,
indent=step.indent,
line_number=step.line_number,
keyword=step.keyword,
)
for step in self._steps
]
steps = background_steps + scenario_steps
return Scenario(feature=self.feature, name=self.name, line_number=self.line_number, steps=steps, tags=self.tags)


class Scenario:

"""Scenario."""

def __init__(self, feature: Feature, name: str, line_number: int, steps: list[Step], tags=None) -> None:
def __init__(self, feature: Feature, name: str, line_number: int, steps: list[Step] = None, tags=None) -> 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.
"""
if steps is None:
steps = []
self.feature = feature
self.name = name
self.steps = steps
Expand Down

0 comments on commit 2411ac2

Please sign in to comment.