forked from test_framework/pytest-bdd
code cleanup. vertical example table implemented
This commit is contained in:
parent
c0ae33a241
commit
482482b059
|
@ -5,7 +5,9 @@ Changelog
|
||||||
-----
|
-----
|
||||||
|
|
||||||
- Pure pytest parametrization for scenario outlines (bubenkoff)
|
- Pure pytest parametrization for scenario outlines (bubenkoff)
|
||||||
- Splitting scenario decorated and non-decorated variants (bubenkoff)
|
- Argumented steps now support converters (transformations) (bubenkoff)
|
||||||
|
- scenario supports only decorator form (bubenkoff)
|
||||||
|
- Code generation refactoring and cleanup (bubenkoff)
|
||||||
|
|
||||||
|
|
||||||
1.0.0
|
1.0.0
|
||||||
|
|
91
README.rst
91
README.rst
|
@ -23,7 +23,7 @@ containing the side effects of the Gherkin imperative declarations.
|
||||||
|
|
||||||
|
|
||||||
Install pytest-bdd
|
Install pytest-bdd
|
||||||
==================
|
------------------
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ Install pytest-bdd
|
||||||
|
|
||||||
|
|
||||||
Example
|
Example
|
||||||
=======
|
-------
|
||||||
|
|
||||||
publish\_article.feature:
|
publish\_article.feature:
|
||||||
|
|
||||||
|
@ -54,7 +54,9 @@ test\_publish\_article.py:
|
||||||
|
|
||||||
from pytest_bdd import scenario, given, when, then
|
from pytest_bdd import scenario, given, when, then
|
||||||
|
|
||||||
test_publish = scenario('publish_article.feature', 'Publishing the article')
|
@scenario('publish_article.feature', 'Publishing the article')
|
||||||
|
def test_publish():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@given('I have an article')
|
@given('I have an article')
|
||||||
|
@ -85,7 +87,7 @@ test\_publish\_article.py:
|
||||||
|
|
||||||
|
|
||||||
Step aliases
|
Step aliases
|
||||||
============
|
------------
|
||||||
|
|
||||||
Sometimes it is needed to declare the same fixtures or steps with the
|
Sometimes it is needed to declare the same fixtures or steps with the
|
||||||
different names for better readability. In order to use the same step
|
different names for better readability. In order to use the same step
|
||||||
|
@ -118,7 +120,7 @@ default author.
|
||||||
|
|
||||||
|
|
||||||
Step arguments
|
Step arguments
|
||||||
==============
|
--------------
|
||||||
|
|
||||||
Often it's possible to reuse steps giving them a parameter(s).
|
Often it's possible to reuse steps giving them a parameter(s).
|
||||||
This allows to have single implementation and multiple use, so less code.
|
This allows to have single implementation and multiple use, so less code.
|
||||||
|
@ -145,7 +147,11 @@ The code will look like:
|
||||||
import re
|
import re
|
||||||
from pytest_bdd import scenario, given, when, then
|
from pytest_bdd import scenario, given, when, then
|
||||||
|
|
||||||
test_arguments = scenario('arguments.feature', 'Arguments for given, when, thens')
|
|
||||||
|
@scenario('arguments.feature', 'Arguments for given, when, thens')
|
||||||
|
def test_arguments():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@given(re.compile('there are (?P<start>\d+) cucumbers'), converters=dict(start=int))
|
@given(re.compile('there are (?P<start>\d+) cucumbers'), converters=dict(start=int))
|
||||||
def start_cucumbers(start):
|
def start_cucumbers(start):
|
||||||
|
@ -167,15 +173,15 @@ different than strings.
|
||||||
|
|
||||||
|
|
||||||
Scenario parameters
|
Scenario parameters
|
||||||
===================
|
-------------------
|
||||||
Scenario function/decorator can accept such optional keyword arguments:
|
Scenario decorator can accept such optional keyword arguments:
|
||||||
|
|
||||||
* `encoding` - decode content of feature file in specific encoding. UTF-8 is default.
|
* `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.
|
* `example_converters` - mapping to pass functions to convert example values provided in feature files.
|
||||||
|
|
||||||
|
|
||||||
Scenario outlines
|
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>.
|
||||||
|
@ -195,6 +201,24 @@ Example:
|
||||||
| start | eat | left |
|
| start | eat | left |
|
||||||
| 12 | 5 | 7 |
|
| 12 | 5 | 7 |
|
||||||
|
|
||||||
|
pytest-bdd feature file format also supports example tables in different way:
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: feature
|
||||||
|
|
||||||
|
Scenario Outline: Outlined given, when, thens
|
||||||
|
Given there are <start> cucumbers
|
||||||
|
When I eat <eat> cucumbers
|
||||||
|
Then I should have <left> cucumbers
|
||||||
|
|
||||||
|
Examples: Vertical
|
||||||
|
| start | 12 | 2 |
|
||||||
|
| eat | 5 | 1 |
|
||||||
|
| left | 7 | 1 |
|
||||||
|
|
||||||
|
This form allows to have tables with lots of columns keeping the maximum text width predictable without significant
|
||||||
|
readability change.
|
||||||
|
|
||||||
|
|
||||||
The code will look like:
|
The code will look like:
|
||||||
|
|
||||||
|
@ -203,11 +227,13 @@ The code will look like:
|
||||||
from pytest_bdd import given, when, then, scenario
|
from pytest_bdd import given, when, then, scenario
|
||||||
|
|
||||||
|
|
||||||
test_outlined = scenario(
|
@scenario(
|
||||||
'outline.feature',
|
'outline.feature',
|
||||||
'Outlined given, when, thens',
|
'Outlined given, when, thens',
|
||||||
example_converters=dict(start=int, eat=float, left=str)
|
example_converters=dict(start=int, eat=float, left=str)
|
||||||
)
|
)
|
||||||
|
def test_outlined():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@given('there are <start> cucumbers')
|
@given('there are <start> cucumbers')
|
||||||
|
@ -242,6 +268,7 @@ The code will look like:
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_bdd import mark, given, when, then
|
from pytest_bdd import mark, given, when, then
|
||||||
|
|
||||||
|
|
||||||
# Here we use pytest to parametrize the test with the parameters table
|
# Here we use pytest to parametrize the test with the parameters table
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
['start', 'eat', 'left'],
|
['start', 'eat', 'left'],
|
||||||
|
@ -276,7 +303,7 @@ The significant downside of this approach is inability to see the test table fro
|
||||||
|
|
||||||
|
|
||||||
Test setup
|
Test setup
|
||||||
==========
|
----------
|
||||||
|
|
||||||
Test setup is implemented within the Given section. Even though these steps
|
Test setup is implemented within the Given section. Even though these steps
|
||||||
are executed imperatively to apply possible side-effects, pytest-bdd is trying
|
are executed imperatively to apply possible side-effects, pytest-bdd is trying
|
||||||
|
@ -366,7 +393,7 @@ Will raise an exception if the step is using the regular expression pattern.
|
||||||
|
|
||||||
|
|
||||||
Reusing fixtures
|
Reusing fixtures
|
||||||
================
|
----------------
|
||||||
|
|
||||||
Sometimes scenarios define new names for the fixture that can be
|
Sometimes scenarios define new names for the fixture that can be
|
||||||
inherited. Fixtures can be reused with other names using given():
|
inherited. Fixtures can be reused with other names using given():
|
||||||
|
@ -377,7 +404,7 @@ inherited. Fixtures can be reused with other names using given():
|
||||||
|
|
||||||
|
|
||||||
Reusing steps
|
Reusing steps
|
||||||
=============
|
-------------
|
||||||
|
|
||||||
It is possible to define some common steps in the parent conftest.py and
|
It is possible to define some common steps in the parent conftest.py and
|
||||||
simply expect them in the child test file.
|
simply expect them in the child test file.
|
||||||
|
@ -410,14 +437,16 @@ test\_common.py:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
test_conftest = scenario('common_steps.feature', 'All steps are declared in the conftest')
|
@scenario('common_steps.feature', 'All steps are declared in the conftest')
|
||||||
|
def test_conftest():
|
||||||
|
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 conftests.
|
||||||
|
|
||||||
|
|
||||||
Feature file paths
|
Feature file paths
|
||||||
==================
|
------------------
|
||||||
|
|
||||||
But default, pytest-bdd will use current module’s path as base path for
|
But default, pytest-bdd will use current module’s path as base path for
|
||||||
finding feature files, but this behaviour can be changed by having
|
finding feature files, but this behaviour can be changed by having
|
||||||
|
@ -436,11 +465,14 @@ test\_publish\_article.py:
|
||||||
def pytestbdd_feature_base_dir():
|
def pytestbdd_feature_base_dir():
|
||||||
return '/home/user/projects/foo.bar/features'
|
return '/home/user/projects/foo.bar/features'
|
||||||
|
|
||||||
test_publish = scenario('publish_article.feature', 'Publishing the article')
|
|
||||||
|
@scenario('publish_article.feature', 'Publishing the article')
|
||||||
|
def test_publish():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
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.
|
This will make your life much easier when defining multiple scenarios in a test file.
|
||||||
|
@ -459,15 +491,22 @@ test\_publish\_article.py:
|
||||||
|
|
||||||
scenario = partial(pytest_bdd.scenario, '/path/to/publish_article.feature')
|
scenario = partial(pytest_bdd.scenario, '/path/to/publish_article.feature')
|
||||||
|
|
||||||
test_publish = scenario('Publishing the article')
|
|
||||||
test_publish_unprivileged = scenario('Publishing the article as unprivileged user')
|
@scenario('Publishing the article')
|
||||||
|
def test_publish():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@scenario('Publishing the article as unprivileged user')
|
||||||
|
def test_publish_unprivileged():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
You can learn more about `functools.partial <http://docs.python.org/2/library/functools.html#functools.partial>`_ in the Python docs.
|
You can learn more about `functools.partial <http://docs.python.org/2/library/functools.html#functools.partial>`_ in the Python docs.
|
||||||
|
|
||||||
|
|
||||||
Hooks
|
Hooks
|
||||||
=====
|
-----
|
||||||
|
|
||||||
pytest-bdd exposes several pytest `hooks <http://pytest.org/latest/plugins.html#well-specified-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:
|
which might be helpful building useful reporting, visualization, etc on top of it:
|
||||||
|
@ -488,7 +527,15 @@ which might be helpful building useful reporting, visualization, etc on top of i
|
||||||
|
|
||||||
|
|
||||||
Browser testing
|
Browser testing
|
||||||
===============
|
---------------
|
||||||
|
|
||||||
|
Tools recommended to use for browser testing:
|
||||||
|
|
||||||
|
* pytest-splinter - pytest splinter integration for the real browser testing
|
||||||
|
|
||||||
|
|
||||||
|
Migration of your tests from versions 0.x.x-1.x.x
|
||||||
|
-------------------------------------------------
|
||||||
|
|
||||||
Tools recommended to use for browser testing:
|
Tools recommended to use for browser testing:
|
||||||
|
|
||||||
|
@ -496,7 +543,7 @@ Tools recommended to use for browser testing:
|
||||||
|
|
||||||
|
|
||||||
License
|
License
|
||||||
=======
|
-------
|
||||||
|
|
||||||
This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_.
|
This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
"""pytest-bdd Exceptions."""
|
||||||
|
|
||||||
|
|
||||||
|
class ScenarioIsDecoratorOnly(Exception):
|
||||||
|
"""Scenario can be only used as decorator."""
|
||||||
|
|
||||||
|
|
||||||
|
class ScenarioValidationError(Exception):
|
||||||
|
"""Base class for scenario validation."""
|
||||||
|
|
||||||
|
|
||||||
|
class ScenarioNotFound(ScenarioValidationError): # pragma: no cover
|
||||||
|
"""Scenario Not Found"""
|
||||||
|
|
||||||
|
|
||||||
|
class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover
|
||||||
|
"""Scenario steps argumets do not match declared scenario examples."""
|
||||||
|
|
||||||
|
|
||||||
|
class StepTypeError(ScenarioValidationError): # pragma: no cover
|
||||||
|
"""Step definition is not of the type expected in the scenario."""
|
||||||
|
|
||||||
|
|
||||||
|
class GivenAlreadyUsed(ScenarioValidationError): # pragma: no cover
|
||||||
|
"""Fixture that implements the Given has been already used."""
|
|
@ -26,10 +26,8 @@ one line.
|
||||||
import re # pragma: no cover
|
import re # pragma: no cover
|
||||||
import sys # pragma: no cover
|
import sys # pragma: no cover
|
||||||
|
|
||||||
from pytest_bdd.types import (
|
from pytest_bdd import types # pragma: no cover
|
||||||
FEATURE, SCENARIO, SCENARIO_OUTLINE, EXAMPLES, EXAMPLES_HEADERS, EXAMPLE_LINE, GIVEN, WHEN,
|
from pytest_bdd import exceptions # pragma: no cover
|
||||||
THEN # pragma: no cover
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FeatureError(Exception): # pragma: no cover
|
class FeatureError(Exception): # pragma: no cover
|
||||||
|
@ -48,16 +46,17 @@ class FeatureError(Exception): # pragma: no cover
|
||||||
features = {} # pragma: no cover
|
features = {} # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
STEP_PREFIXES = { # pragma: no cover
|
STEP_PREFIXES = [ # pragma: no cover
|
||||||
'Feature: ': FEATURE,
|
('Feature: ', types.FEATURE),
|
||||||
'Scenario Outline: ': SCENARIO_OUTLINE,
|
('Scenario Outline: ', types.SCENARIO_OUTLINE),
|
||||||
'Examples:': EXAMPLES,
|
('Examples: Vertical', types.EXAMPLES_VERTICAL),
|
||||||
'Scenario: ': SCENARIO,
|
('Examples:', types.EXAMPLES),
|
||||||
'Given ': GIVEN,
|
('Scenario: ', types.SCENARIO),
|
||||||
'When ': WHEN,
|
('Given ', types.GIVEN),
|
||||||
'Then ': THEN,
|
('When ', types.WHEN),
|
||||||
'And ': None, # Unknown step type
|
('Then ', types.THEN),
|
||||||
}
|
('And ', None), # Unknown step type
|
||||||
|
]
|
||||||
|
|
||||||
COMMENT_SYMBOLS = '#' # pragma: no cover
|
COMMENT_SYMBOLS = '#' # pragma: no cover
|
||||||
|
|
||||||
|
@ -70,9 +69,9 @@ def get_step_type(line):
|
||||||
:param line: Line of the Feature file
|
:param line: Line of the Feature file
|
||||||
:return: SCENARIO, GIVEN, WHEN, THEN, or `None` if can't be detected.
|
:return: SCENARIO, GIVEN, WHEN, THEN, or `None` if can't be detected.
|
||||||
"""
|
"""
|
||||||
for prefix in STEP_PREFIXES:
|
for prefix, _type in STEP_PREFIXES:
|
||||||
if line.startswith(prefix):
|
if line.startswith(prefix):
|
||||||
return STEP_PREFIXES[prefix]
|
return _type
|
||||||
|
|
||||||
|
|
||||||
def get_step_params(name):
|
def get_step_params(name):
|
||||||
|
@ -104,7 +103,7 @@ def remove_prefix(line):
|
||||||
:return: Line without the prefix.
|
:return: Line without the prefix.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for prefix in STEP_PREFIXES:
|
for prefix, _ in STEP_PREFIXES:
|
||||||
if line.startswith(prefix):
|
if line.startswith(prefix):
|
||||||
return line[len(prefix):].strip()
|
return line[len(prefix):].strip()
|
||||||
return line
|
return line
|
||||||
|
@ -156,20 +155,21 @@ class Feature(object):
|
||||||
continue
|
continue
|
||||||
mode = get_step_type(line) or mode
|
mode = get_step_type(line) or mode
|
||||||
|
|
||||||
if mode == GIVEN and prev_mode not in (GIVEN, SCENARIO, SCENARIO_OUTLINE):
|
if mode == types.GIVEN and prev_mode not in (types.GIVEN, types.SCENARIO, types.SCENARIO_OUTLINE):
|
||||||
raise FeatureError('Given steps must be the first in withing the Scenario',
|
raise FeatureError('Given steps must be the first in withing the Scenario',
|
||||||
line_number, line)
|
line_number, line)
|
||||||
|
|
||||||
if mode == WHEN and prev_mode not in (SCENARIO, SCENARIO_OUTLINE, GIVEN, WHEN):
|
if mode == types.WHEN and prev_mode not in (
|
||||||
|
types.SCENARIO, types.SCENARIO_OUTLINE, types.GIVEN, types.WHEN):
|
||||||
raise FeatureError('When steps must be the first or follow Given steps',
|
raise FeatureError('When steps must be the first or follow Given steps',
|
||||||
line_number, line)
|
line_number, line)
|
||||||
|
|
||||||
if mode == THEN and prev_mode not in (GIVEN, WHEN, THEN):
|
if mode == types.THEN and prev_mode not in (types.GIVEN, types.WHEN, types.THEN):
|
||||||
raise FeatureError('Then steps must follow Given or When steps',
|
raise FeatureError('Then steps must follow Given or When steps',
|
||||||
line_number, line)
|
line_number, line)
|
||||||
|
|
||||||
if mode == FEATURE:
|
if mode == types.FEATURE:
|
||||||
if prev_mode != FEATURE:
|
if prev_mode != types.FEATURE:
|
||||||
self.name = remove_prefix(line)
|
self.name = remove_prefix(line)
|
||||||
else:
|
else:
|
||||||
description.append(line)
|
description.append(line)
|
||||||
|
@ -178,16 +178,21 @@ class Feature(object):
|
||||||
|
|
||||||
# Remove Feature, Given, When, Then, And
|
# Remove Feature, Given, When, Then, And
|
||||||
line = remove_prefix(line)
|
line = remove_prefix(line)
|
||||||
if mode in [SCENARIO, SCENARIO_OUTLINE]:
|
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
|
||||||
self.scenarios[line] = scenario = Scenario(line)
|
self.scenarios[line] = scenario = Scenario(self, line)
|
||||||
elif mode == EXAMPLES:
|
elif mode == types.EXAMPLES:
|
||||||
mode = EXAMPLES_HEADERS
|
mode = types.EXAMPLES_HEADERS
|
||||||
elif mode == EXAMPLES_HEADERS:
|
elif mode == types.EXAMPLES_VERTICAL:
|
||||||
|
mode = types.EXAMPLE_LINE_VERTICAL
|
||||||
|
elif mode == types.EXAMPLES_HEADERS:
|
||||||
scenario.set_param_names([l.strip() for l in line.split('|') if l.strip()])
|
scenario.set_param_names([l.strip() for l in line.split('|') if l.strip()])
|
||||||
mode = EXAMPLE_LINE
|
mode = types.EXAMPLE_LINE
|
||||||
elif mode == EXAMPLE_LINE:
|
elif mode == types.EXAMPLE_LINE:
|
||||||
scenario.add_example([l.strip() for l in line.split('|') if l.strip()])
|
scenario.add_example([l.strip() for l in line.split('|') if l.strip()])
|
||||||
elif mode and mode != FEATURE:
|
elif mode == types.EXAMPLE_LINE_VERTICAL:
|
||||||
|
line = [l.strip() for l in line.split('|') if l.strip()]
|
||||||
|
scenario.add_example_row(line[0], line[1:])
|
||||||
|
elif mode and mode != types.FEATURE:
|
||||||
scenario.add_step(step_name=line, step_type=mode)
|
scenario.add_step(step_name=line, step_type=mode)
|
||||||
|
|
||||||
self.description = u'\n'.join(description)
|
self.description = u'\n'.join(description)
|
||||||
|
@ -215,12 +220,15 @@ class Feature(object):
|
||||||
class Scenario(object):
|
class Scenario(object):
|
||||||
"""Scenario."""
|
"""Scenario."""
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, feature, name, example_converters=None):
|
||||||
|
self.feature = feature
|
||||||
self.name = name
|
self.name = name
|
||||||
self.params = set()
|
self.params = set()
|
||||||
self.steps = []
|
self.steps = []
|
||||||
self.example_params = []
|
self.example_params = []
|
||||||
self.examples = []
|
self.examples = []
|
||||||
|
self.vertical_examples = []
|
||||||
|
self.example_converters = example_converters
|
||||||
|
|
||||||
def add_step(self, step_name, step_type):
|
def add_step(self, step_name, step_type):
|
||||||
"""Add step to the scenario.
|
"""Add step to the scenario.
|
||||||
|
@ -249,6 +257,58 @@ class Scenario(object):
|
||||||
"""
|
"""
|
||||||
self.examples.append(values)
|
self.examples.append(values)
|
||||||
|
|
||||||
|
def add_example_row(self, param, values):
|
||||||
|
"""Add example row.
|
||||||
|
|
||||||
|
:param param: `str` parameter name
|
||||||
|
:param values: `list` of `string` parameter values
|
||||||
|
|
||||||
|
"""
|
||||||
|
if param in self.example_params:
|
||||||
|
raise exceptions.ScenarioExamplesNotValidError(
|
||||||
|
"""Scenario "{0}" in the feature "{1}" has not valid examples. """
|
||||||
|
"""Example rows should contain unique parameters. {2} appeared more than once.""".format(
|
||||||
|
self.name, self.feature.filename, param,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.example_params.append(param)
|
||||||
|
self.vertical_examples.append(values)
|
||||||
|
|
||||||
|
def get_params(self):
|
||||||
|
"""Get scenario pytest parametrization table."""
|
||||||
|
param_count = len(self.example_params)
|
||||||
|
if self.vertical_examples and not self.examples:
|
||||||
|
for value_index in range(len(self.vertical_examples[0])):
|
||||||
|
example = []
|
||||||
|
for param_index in range(param_count):
|
||||||
|
example.append(self.vertical_examples[param_index][value_index])
|
||||||
|
self.examples.append(example)
|
||||||
|
|
||||||
|
if self.examples:
|
||||||
|
params = []
|
||||||
|
for example in self.examples:
|
||||||
|
for index, param in enumerate(self.example_params):
|
||||||
|
if self.example_converters and param in self.example_converters:
|
||||||
|
example[index] = self.example_converters[param](example[index])
|
||||||
|
params.append(example)
|
||||||
|
return [self.example_params, params]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def validate(self):
|
||||||
|
"""Validate the scenario.
|
||||||
|
|
||||||
|
:raises: `ScenarioValidationError`
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.params and self.example_params and self.params != set(self.example_params):
|
||||||
|
raise exceptions.ScenarioExamplesNotValidError(
|
||||||
|
"""Scenario "{0}" in the feature "{1}" has not valid examples. """
|
||||||
|
"""Set of step parameters {2} should match set of example values {3}.""".format(
|
||||||
|
self.name, self.feature.filename, sorted(self.params), sorted(self.example_params),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Step(object):
|
class Step(object):
|
||||||
"""Step."""
|
"""Step."""
|
||||||
|
|
|
@ -26,34 +26,11 @@ from _pytest import python
|
||||||
from pytest_bdd.feature import Feature, force_encode # pragma: no cover
|
from pytest_bdd.feature import Feature, force_encode # pragma: no cover
|
||||||
from pytest_bdd.steps import execute, recreate_function, get_caller_module, get_caller_function
|
from pytest_bdd.steps import execute, recreate_function, get_caller_module, get_caller_function
|
||||||
from pytest_bdd.types import GIVEN
|
from pytest_bdd.types import GIVEN
|
||||||
|
from pytest_bdd import exceptions
|
||||||
|
|
||||||
from pytest_bdd import plugin
|
from pytest_bdd import plugin
|
||||||
|
|
||||||
|
|
||||||
class ScenarioIsDecoratorOnly(Exception):
|
|
||||||
"""Scenario can be only used as decorator."""
|
|
||||||
|
|
||||||
|
|
||||||
class ScenarioValidationError(Exception):
|
|
||||||
"""Base class for scenario validation."""
|
|
||||||
|
|
||||||
|
|
||||||
class ScenarioNotFound(ScenarioValidationError): # pragma: no cover
|
|
||||||
"""Scenario Not Found"""
|
|
||||||
|
|
||||||
|
|
||||||
class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover
|
|
||||||
"""Scenario steps argumets do not match declared scenario examples."""
|
|
||||||
|
|
||||||
|
|
||||||
class StepTypeError(ScenarioValidationError): # pragma: no cover
|
|
||||||
"""Step definition is not of the type expected in the scenario."""
|
|
||||||
|
|
||||||
|
|
||||||
class GivenAlreadyUsed(ScenarioValidationError): # pragma: no cover
|
|
||||||
"""Fixture that implements the Given has been already used."""
|
|
||||||
|
|
||||||
|
|
||||||
def _inject_fixture(request, arg, value):
|
def _inject_fixture(request, arg, value):
|
||||||
"""Inject fixture into pytest fixture request.
|
"""Inject fixture into pytest fixture request.
|
||||||
|
|
||||||
|
@ -122,17 +99,6 @@ def _find_step_function(request, name, encoding):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def _validate_scenario(feature, scenario):
|
|
||||||
"""Validate the scenario."""
|
|
||||||
if scenario.params and scenario.example_params and scenario.params != set(scenario.example_params):
|
|
||||||
raise ScenarioExamplesNotValidError(
|
|
||||||
"""Scenario "{0}" in the feature "{1}" has not valid examples. """
|
|
||||||
"""Set of step parameters {2} should match set of example values {3}.""".format(
|
|
||||||
scenario.name, feature.filename, sorted(scenario.params), sorted(scenario.example_params),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _execute_step_function(request, feature, step, step_func, example=None):
|
def _execute_step_function(request, feature, step, step_func, example=None):
|
||||||
"""Execute step function."""
|
"""Execute step function."""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
@ -176,20 +142,20 @@ def _execute_scenario(feature, scenario, request, encoding, example=None):
|
||||||
try:
|
try:
|
||||||
# Check the step types are called in the correct order
|
# Check the step types are called in the correct order
|
||||||
if step_func.step_type != step.type:
|
if step_func.step_type != step.type:
|
||||||
raise StepTypeError(
|
raise exceptions.StepTypeError(
|
||||||
'Wrong step type "{0}" while "{1}" is expected.'.format(step_func.step_type, step.type)
|
'Wrong step type "{0}" while "{1}" is expected.'.format(step_func.step_type, step.type)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if the fixture that implements given step has not been yet used by another given step
|
# Check if the fixture that implements given step has not been yet used by another given step
|
||||||
if step.type == GIVEN:
|
if step.type == GIVEN:
|
||||||
if step_func.fixture in givens:
|
if step_func.fixture in givens:
|
||||||
raise GivenAlreadyUsed(
|
raise exceptions.GivenAlreadyUsed(
|
||||||
'Fixture "{0}" that implements this "{1}" given step has been already used.'.format(
|
'Fixture "{0}" that implements this "{1}" given step has been already used.'.format(
|
||||||
step_func.fixture, step.name,
|
step_func.fixture, step.name,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
givens.add(step_func.fixture)
|
givens.add(step_func.fixture)
|
||||||
except ScenarioValidationError as exception:
|
except exceptions.ScenarioValidationError as exception:
|
||||||
request.config.hook.pytest_bdd_step_validation_error(
|
request.config.hook.pytest_bdd_step_validation_error(
|
||||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||||
exception=exception)
|
exception=exception)
|
||||||
|
@ -228,47 +194,15 @@ def get_fixture(caller_module, fixture, path=None, module=None):
|
||||||
return get_fixture(caller_module, fixture, path=os.path.dirname(path), module=module)
|
return get_fixture(caller_module, fixture, path=os.path.dirname(path), module=module)
|
||||||
|
|
||||||
|
|
||||||
def scenario(
|
def _get_scenario_decorator(
|
||||||
feature_name, scenario_name, encoding='utf-8', example_converters=None,
|
feature, feature_name, scenario, scenario_name, caller_module, caller_function, encoding):
|
||||||
caller_module=None, caller_function=None):
|
"""Get scenario decorator."""
|
||||||
"""Scenario."""
|
g = locals()
|
||||||
|
g['_execute_scenario'] = _execute_scenario
|
||||||
caller_module = caller_module or get_caller_module()
|
|
||||||
caller_function = caller_function or get_caller_function()
|
|
||||||
|
|
||||||
# Get the feature
|
|
||||||
base_path = get_fixture(caller_module, 'pytestbdd_feature_base_dir')
|
|
||||||
feature_path = op.abspath(op.join(base_path, feature_name))
|
|
||||||
feature = Feature.get_feature(feature_path, encoding=encoding)
|
|
||||||
|
|
||||||
# Get the scenario
|
|
||||||
try:
|
|
||||||
scenario = feature.scenarios[scenario_name]
|
|
||||||
except KeyError:
|
|
||||||
raise ScenarioNotFound(
|
|
||||||
'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate the scenario
|
|
||||||
_validate_scenario(feature, scenario)
|
|
||||||
|
|
||||||
if scenario.examples:
|
|
||||||
params = []
|
|
||||||
for example in scenario.examples:
|
|
||||||
for index, param in enumerate(scenario.example_params):
|
|
||||||
if example_converters and param in example_converters:
|
|
||||||
example[index] = example_converters[param](example[index])
|
|
||||||
params.append(example)
|
|
||||||
params = [scenario.example_params, params]
|
|
||||||
else:
|
|
||||||
params = []
|
|
||||||
|
|
||||||
g = globals().copy()
|
|
||||||
g.update(locals())
|
|
||||||
|
|
||||||
def decorator(_pytestbdd_function):
|
def decorator(_pytestbdd_function):
|
||||||
if isinstance(_pytestbdd_function, python.FixtureRequest):
|
if isinstance(_pytestbdd_function, python.FixtureRequest):
|
||||||
raise ScenarioIsDecoratorOnly(
|
raise exceptions.ScenarioIsDecoratorOnly(
|
||||||
'scenario function can only be used as a decorator. Refer to the documentation.')
|
'scenario function can only be used as a decorator. Refer to the documentation.')
|
||||||
|
|
||||||
g.update(locals())
|
g.update(locals())
|
||||||
|
@ -293,6 +227,8 @@ def scenario(
|
||||||
_scenario = recreate_function(
|
_scenario = recreate_function(
|
||||||
g[_pytestbdd_function.__name__], module=caller_module, firstlineno=caller_function.f_lineno)
|
g[_pytestbdd_function.__name__], module=caller_module, firstlineno=caller_function.f_lineno)
|
||||||
|
|
||||||
|
params = scenario.get_params()
|
||||||
|
|
||||||
if params:
|
if params:
|
||||||
_scenario = pytest.mark.parametrize(*params)(_scenario)
|
_scenario = pytest.mark.parametrize(*params)(_scenario)
|
||||||
|
|
||||||
|
@ -300,6 +236,34 @@ def scenario(
|
||||||
feature_name=feature_name, scenario_name=scenario_name)
|
feature_name=feature_name, scenario_name=scenario_name)
|
||||||
return _scenario
|
return _scenario
|
||||||
|
|
||||||
decorator = recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno)
|
return recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno)
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
def scenario(
|
||||||
|
feature_name, scenario_name, encoding='utf-8', example_converters=None,
|
||||||
|
caller_module=None, caller_function=None):
|
||||||
|
"""Scenario."""
|
||||||
|
|
||||||
|
caller_module = caller_module or get_caller_module()
|
||||||
|
caller_function = caller_function or get_caller_function()
|
||||||
|
|
||||||
|
# Get the feature
|
||||||
|
base_path = get_fixture(caller_module, 'pytestbdd_feature_base_dir')
|
||||||
|
feature_path = op.abspath(op.join(base_path, feature_name))
|
||||||
|
feature = Feature.get_feature(feature_path, encoding=encoding)
|
||||||
|
|
||||||
|
# Get the scenario
|
||||||
|
try:
|
||||||
|
scenario = feature.scenarios[scenario_name]
|
||||||
|
except KeyError:
|
||||||
|
raise exceptions.ScenarioNotFound(
|
||||||
|
'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
scenario.example_converters = example_converters
|
||||||
|
|
||||||
|
# Validate the scenario
|
||||||
|
scenario.validate()
|
||||||
|
|
||||||
|
return _get_scenario_decorator(
|
||||||
|
feature, feature_name, scenario, scenario_name, caller_module, caller_function, encoding)
|
||||||
|
|
|
@ -3,8 +3,10 @@
|
||||||
FEATURE = 'feature' # pragma: no cover
|
FEATURE = 'feature' # pragma: no cover
|
||||||
SCENARIO_OUTLINE = 'scenario outline' # pragma: no cover
|
SCENARIO_OUTLINE = 'scenario outline' # pragma: no cover
|
||||||
EXAMPLES = 'examples' # pragma: no cover
|
EXAMPLES = 'examples' # pragma: no cover
|
||||||
|
EXAMPLES_VERTICAL = 'examples vertical' # pragma: no cover
|
||||||
EXAMPLES_HEADERS = 'example headers' # pragma: no cover
|
EXAMPLES_HEADERS = 'example headers' # pragma: no cover
|
||||||
EXAMPLE_LINE = 'example line' # pragma: no cover
|
EXAMPLE_LINE = 'example line' # pragma: no cover
|
||||||
|
EXAMPLE_LINE_VERTICAL = 'example line vertical' # pragma: no cover
|
||||||
SCENARIO = 'scenario' # pragma: no cover
|
SCENARIO = 'scenario' # pragma: no cover
|
||||||
GIVEN = 'given' # pragma: no cover
|
GIVEN = 'given' # pragma: no cover
|
||||||
WHEN = 'when' # pragma: no cover
|
WHEN = 'when' # pragma: no cover
|
||||||
|
|
|
@ -4,7 +4,7 @@ import functools
|
||||||
import re
|
import re
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_bdd import scenario, given, when, then
|
from pytest_bdd import scenario, given, when, then
|
||||||
from pytest_bdd.scenario import GivenAlreadyUsed
|
from pytest_bdd import exceptions
|
||||||
|
|
||||||
|
|
||||||
@scenario(
|
@scenario(
|
||||||
|
@ -73,5 +73,5 @@ def test_multiple_given(request):
|
||||||
)
|
)
|
||||||
def test():
|
def test():
|
||||||
pass
|
pass
|
||||||
with pytest.raises(GivenAlreadyUsed):
|
with pytest.raises(exceptions.GivenAlreadyUsed):
|
||||||
test(request)
|
test(request)
|
||||||
|
|
|
@ -28,3 +28,14 @@ Scenario Outline: Outlined with some examples failing
|
||||||
| start | eat | left |
|
| start | eat | left |
|
||||||
| 0 | 5 | 5 |
|
| 0 | 5 | 5 |
|
||||||
| 12 | 5 | 7 |
|
| 12 | 5 | 7 |
|
||||||
|
|
||||||
|
|
||||||
|
Scenario Outline: Outlined with vertical example table
|
||||||
|
Given there are <start> cucumbers
|
||||||
|
When I eat <eat> cucumbers
|
||||||
|
Then I should have <left> cucumbers
|
||||||
|
|
||||||
|
Examples: Vertical
|
||||||
|
| start | 12 | 2 |
|
||||||
|
| eat | 5 | 1 |
|
||||||
|
| left | 7 | 1 |
|
||||||
|
|
|
@ -4,7 +4,7 @@ import re
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pytest_bdd import given, when, then, scenario
|
from pytest_bdd import given, when, then, scenario
|
||||||
from pytest_bdd.scenario import ScenarioExamplesNotValidError
|
from pytest_bdd import exceptions
|
||||||
|
|
||||||
|
|
||||||
@scenario(
|
@scenario(
|
||||||
|
@ -13,7 +13,8 @@ from pytest_bdd.scenario import ScenarioExamplesNotValidError
|
||||||
example_converters=dict(start=int, eat=float, left=str)
|
example_converters=dict(start=int, eat=float, left=str)
|
||||||
)
|
)
|
||||||
def test_outlined():
|
def test_outlined():
|
||||||
assert 1
|
assert test_outlined.parametrize.args == (
|
||||||
|
[u'start', u'eat', u'left'], [[12, 5.0, '7'], [5, 4.0, '1']])
|
||||||
|
|
||||||
|
|
||||||
@given('there are <start> cucumbers')
|
@given('there are <start> cucumbers')
|
||||||
|
@ -39,7 +40,7 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||||
def test_wrongly_outlined(request):
|
def test_wrongly_outlined(request):
|
||||||
"""Test parametrized scenario when the test function lacks parameters."""
|
"""Test parametrized scenario when the test function lacks parameters."""
|
||||||
|
|
||||||
with pytest.raises(ScenarioExamplesNotValidError) as exc:
|
with pytest.raises(exceptions.ScenarioExamplesNotValidError) as exc:
|
||||||
@scenario(
|
@scenario(
|
||||||
'outline.feature',
|
'outline.feature',
|
||||||
'Outlined with wrong examples',
|
'Outlined with wrong examples',
|
||||||
|
@ -67,3 +68,14 @@ def other_fixture(request):
|
||||||
)
|
)
|
||||||
def test_outlined_with_other_fixtures(other_fixture):
|
def test_outlined_with_other_fixtures(other_fixture):
|
||||||
"""Test outlined scenario also using other parametrized fixture."""
|
"""Test outlined scenario also using other parametrized fixture."""
|
||||||
|
|
||||||
|
|
||||||
|
@scenario(
|
||||||
|
'outline.feature',
|
||||||
|
'Outlined with vertical example table',
|
||||||
|
example_converters=dict(start=int, eat=float, left=str)
|
||||||
|
)
|
||||||
|
def test_vertical_example():
|
||||||
|
"""Test outlined scenario with vertical examples table."""
|
||||||
|
assert test_vertical_example.parametrize.args == (
|
||||||
|
[u'start', u'eat', u'left'], [[12, 5.0, '7'], [2, 1.0, '1']])
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
|
"""Test scenario decorator."""
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pytest_bdd import scenario
|
from pytest_bdd import scenario
|
||||||
from pytest_bdd.scenario import ScenarioNotFound
|
from pytest_bdd import exceptions
|
||||||
|
|
||||||
|
|
||||||
def test_scenario_not_found(request):
|
def test_scenario_not_found(request):
|
||||||
"""Test the situation when scenario is not found."""
|
"""Test the situation when scenario is not found."""
|
||||||
|
|
||||||
with pytest.raises(ScenarioNotFound):
|
with pytest.raises(exceptions.ScenarioNotFound):
|
||||||
scenario(
|
scenario(
|
||||||
'not_found.feature',
|
'not_found.feature',
|
||||||
'NOT FOUND'
|
'NOT FOUND'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pytest_bdd import scenario, given, when, then
|
from pytest_bdd import scenario, given, when, then
|
||||||
from pytest_bdd.scenario import GivenAlreadyUsed
|
from pytest_bdd import exceptions
|
||||||
|
|
||||||
|
|
||||||
@scenario('steps.feature', 'Executed step by step')
|
@scenario('steps.feature', 'Executed step by step')
|
||||||
|
@ -84,7 +84,7 @@ def test_multiple_given(request):
|
||||||
def test():
|
def test():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with pytest.raises(GivenAlreadyUsed):
|
with pytest.raises(exceptions.GivenAlreadyUsed):
|
||||||
test(request)
|
test(request)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""Test wrong feature syntax."""
|
"""Test wrong feature syntax."""
|
||||||
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pytest_bdd import scenario, given, when, then
|
from pytest_bdd import scenario, given, when, then
|
||||||
from pytest_bdd.feature import FeatureError
|
from pytest_bdd.feature import FeatureError
|
||||||
from pytest_bdd.scenario import StepTypeError
|
from pytest_bdd import exceptions
|
||||||
|
|
||||||
|
|
||||||
@given('something')
|
@given('something')
|
||||||
|
@ -57,10 +59,9 @@ def test_wrong_type_order(request, scenario_name):
|
||||||
def test_wrong_type_order(request):
|
def test_wrong_type_order(request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
with pytest.raises(StepTypeError) as excinfo:
|
with pytest.raises(exceptions.StepTypeError) as excinfo:
|
||||||
test_wrong_type_order(request)
|
test_wrong_type_order(request)
|
||||||
|
assert re.match(r'Wrong step type \"(\w+)\" while \"(\w+)\" is expected\.', excinfo.value.args[0])
|
||||||
excinfo # TODO: assert the exception args from parameters
|
|
||||||
|
|
||||||
|
|
||||||
def test_verbose_output(request):
|
def test_verbose_output(request):
|
||||||
|
|
Loading…
Reference in New Issue