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)
|
||||
- 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
|
||||
|
|
91
README.rst
91
README.rst
|
@ -23,7 +23,7 @@ containing the side effects of the Gherkin imperative declarations.
|
|||
|
||||
|
||||
Install pytest-bdd
|
||||
==================
|
||||
------------------
|
||||
|
||||
::
|
||||
|
||||
|
@ -31,7 +31,7 @@ Install pytest-bdd
|
|||
|
||||
|
||||
Example
|
||||
=======
|
||||
-------
|
||||
|
||||
publish\_article.feature:
|
||||
|
||||
|
@ -54,7 +54,9 @@ test\_publish\_article.py:
|
|||
|
||||
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')
|
||||
|
@ -85,7 +87,7 @@ test\_publish\_article.py:
|
|||
|
||||
|
||||
Step aliases
|
||||
============
|
||||
------------
|
||||
|
||||
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
|
||||
|
@ -118,7 +120,7 @@ default author.
|
|||
|
||||
|
||||
Step arguments
|
||||
==============
|
||||
--------------
|
||||
|
||||
Often it's possible to reuse steps giving them a parameter(s).
|
||||
This allows to have single implementation and multiple use, so less code.
|
||||
|
@ -145,7 +147,11 @@ The code will look like:
|
|||
import re
|
||||
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))
|
||||
def start_cucumbers(start):
|
||||
|
@ -167,15 +173,15 @@ different than strings.
|
|||
|
||||
|
||||
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.
|
||||
* `example_converters` - mapping to pass functions to convert example values provided in feature files.
|
||||
|
||||
|
||||
Scenario outlines
|
||||
=================
|
||||
-----------------
|
||||
|
||||
Scenarios can be parametrized to cover few cases. In Gherkin the variable
|
||||
templates are written using corner braces as <somevalue>.
|
||||
|
@ -195,6 +201,24 @@ Example:
|
|||
| start | eat | left |
|
||||
| 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:
|
||||
|
||||
|
@ -203,11 +227,13 @@ The code will look like:
|
|||
from pytest_bdd import given, when, then, scenario
|
||||
|
||||
|
||||
test_outlined = scenario(
|
||||
@scenario(
|
||||
'outline.feature',
|
||||
'Outlined given, when, thens',
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_outlined():
|
||||
pass
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
|
@ -242,6 +268,7 @@ The code will look like:
|
|||
import pytest
|
||||
from pytest_bdd import mark, given, when, then
|
||||
|
||||
|
||||
# Here we use pytest to parametrize the test with the parameters table
|
||||
@pytest.mark.parametrize(
|
||||
['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 is implemented within the Given section. Even though these steps
|
||||
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
|
||||
================
|
||||
----------------
|
||||
|
||||
Sometimes scenarios define new names for the fixture that can be
|
||||
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
|
||||
=============
|
||||
-------------
|
||||
|
||||
It is possible to define some common steps in the parent conftest.py and
|
||||
simply expect them in the child test file.
|
||||
|
@ -410,14 +437,16 @@ test\_common.py:
|
|||
|
||||
.. 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
|
||||
collected from the parent conftests.
|
||||
|
||||
|
||||
Feature file paths
|
||||
==================
|
||||
------------------
|
||||
|
||||
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
|
||||
|
@ -436,11 +465,14 @@ test\_publish\_article.py:
|
|||
def pytestbdd_feature_base_dir():
|
||||
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
|
||||
====================================
|
||||
------------------------------------
|
||||
|
||||
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.
|
||||
|
@ -459,15 +491,22 @@ test\_publish\_article.py:
|
|||
|
||||
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.
|
||||
|
||||
|
||||
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:
|
||||
|
@ -488,7 +527,15 @@ which might be helpful building useful reporting, visualization, etc on top of i
|
|||
|
||||
|
||||
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:
|
||||
|
||||
|
@ -496,7 +543,7 @@ Tools recommended to use for browser testing:
|
|||
|
||||
|
||||
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 sys # pragma: no cover
|
||||
|
||||
from pytest_bdd.types import (
|
||||
FEATURE, SCENARIO, SCENARIO_OUTLINE, EXAMPLES, EXAMPLES_HEADERS, EXAMPLE_LINE, GIVEN, WHEN,
|
||||
THEN # pragma: no cover
|
||||
)
|
||||
from pytest_bdd import types # pragma: no cover
|
||||
from pytest_bdd import exceptions # pragma: no cover
|
||||
|
||||
|
||||
class FeatureError(Exception): # pragma: no cover
|
||||
|
@ -48,16 +46,17 @@ class FeatureError(Exception): # pragma: no cover
|
|||
features = {} # pragma: no cover
|
||||
|
||||
|
||||
STEP_PREFIXES = { # pragma: no cover
|
||||
'Feature: ': FEATURE,
|
||||
'Scenario Outline: ': SCENARIO_OUTLINE,
|
||||
'Examples:': EXAMPLES,
|
||||
'Scenario: ': SCENARIO,
|
||||
'Given ': GIVEN,
|
||||
'When ': WHEN,
|
||||
'Then ': THEN,
|
||||
'And ': None, # Unknown step type
|
||||
}
|
||||
STEP_PREFIXES = [ # pragma: no cover
|
||||
('Feature: ', types.FEATURE),
|
||||
('Scenario Outline: ', types.SCENARIO_OUTLINE),
|
||||
('Examples: Vertical', types.EXAMPLES_VERTICAL),
|
||||
('Examples:', types.EXAMPLES),
|
||||
('Scenario: ', types.SCENARIO),
|
||||
('Given ', types.GIVEN),
|
||||
('When ', types.WHEN),
|
||||
('Then ', types.THEN),
|
||||
('And ', None), # Unknown step type
|
||||
]
|
||||
|
||||
COMMENT_SYMBOLS = '#' # pragma: no cover
|
||||
|
||||
|
@ -70,9 +69,9 @@ def get_step_type(line):
|
|||
:param line: Line of the Feature file
|
||||
: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):
|
||||
return STEP_PREFIXES[prefix]
|
||||
return _type
|
||||
|
||||
|
||||
def get_step_params(name):
|
||||
|
@ -104,7 +103,7 @@ def remove_prefix(line):
|
|||
:return: Line without the prefix.
|
||||
|
||||
"""
|
||||
for prefix in STEP_PREFIXES:
|
||||
for prefix, _ in STEP_PREFIXES:
|
||||
if line.startswith(prefix):
|
||||
return line[len(prefix):].strip()
|
||||
return line
|
||||
|
@ -156,20 +155,21 @@ class Feature(object):
|
|||
continue
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
line_number, line)
|
||||
|
||||
if mode == FEATURE:
|
||||
if prev_mode != FEATURE:
|
||||
if mode == types.FEATURE:
|
||||
if prev_mode != types.FEATURE:
|
||||
self.name = remove_prefix(line)
|
||||
else:
|
||||
description.append(line)
|
||||
|
@ -178,16 +178,21 @@ class Feature(object):
|
|||
|
||||
# Remove Feature, Given, When, Then, And
|
||||
line = remove_prefix(line)
|
||||
if mode in [SCENARIO, SCENARIO_OUTLINE]:
|
||||
self.scenarios[line] = scenario = Scenario(line)
|
||||
elif mode == EXAMPLES:
|
||||
mode = EXAMPLES_HEADERS
|
||||
elif mode == EXAMPLES_HEADERS:
|
||||
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
|
||||
self.scenarios[line] = scenario = Scenario(self, line)
|
||||
elif mode == types.EXAMPLES:
|
||||
mode = types.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()])
|
||||
mode = EXAMPLE_LINE
|
||||
elif mode == EXAMPLE_LINE:
|
||||
mode = types.EXAMPLE_LINE
|
||||
elif mode == types.EXAMPLE_LINE:
|
||||
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)
|
||||
|
||||
self.description = u'\n'.join(description)
|
||||
|
@ -215,12 +220,15 @@ class Feature(object):
|
|||
class Scenario(object):
|
||||
"""Scenario."""
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, feature, name, example_converters=None):
|
||||
self.feature = feature
|
||||
self.name = name
|
||||
self.params = set()
|
||||
self.steps = []
|
||||
self.example_params = []
|
||||
self.examples = []
|
||||
self.vertical_examples = []
|
||||
self.example_converters = example_converters
|
||||
|
||||
def add_step(self, step_name, step_type):
|
||||
"""Add step to the scenario.
|
||||
|
@ -249,6 +257,58 @@ class Scenario(object):
|
|||
"""
|
||||
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):
|
||||
"""Step."""
|
||||
|
|
|
@ -26,34 +26,11 @@ from _pytest import python
|
|||
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.types import GIVEN
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
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):
|
||||
"""Inject fixture into pytest fixture request.
|
||||
|
||||
|
@ -122,17 +99,6 @@ def _find_step_function(request, name, encoding):
|
|||
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):
|
||||
"""Execute step function."""
|
||||
kwargs = {}
|
||||
|
@ -176,20 +142,20 @@ def _execute_scenario(feature, scenario, request, encoding, example=None):
|
|||
try:
|
||||
# Check the step types are called in the correct order
|
||||
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)
|
||||
)
|
||||
|
||||
# Check if the fixture that implements given step has not been yet used by another given step
|
||||
if step.type == GIVEN:
|
||||
if step_func.fixture in givens:
|
||||
raise GivenAlreadyUsed(
|
||||
raise exceptions.GivenAlreadyUsed(
|
||||
'Fixture "{0}" that implements this "{1}" given step has been already used.'.format(
|
||||
step_func.fixture, step.name,
|
||||
)
|
||||
)
|
||||
givens.add(step_func.fixture)
|
||||
except ScenarioValidationError as exception:
|
||||
except exceptions.ScenarioValidationError as exception:
|
||||
request.config.hook.pytest_bdd_step_validation_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
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)
|
||||
|
||||
|
||||
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 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 _get_scenario_decorator(
|
||||
feature, feature_name, scenario, scenario_name, caller_module, caller_function, encoding):
|
||||
"""Get scenario decorator."""
|
||||
g = locals()
|
||||
g['_execute_scenario'] = _execute_scenario
|
||||
|
||||
def decorator(_pytestbdd_function):
|
||||
if isinstance(_pytestbdd_function, python.FixtureRequest):
|
||||
raise ScenarioIsDecoratorOnly(
|
||||
raise exceptions.ScenarioIsDecoratorOnly(
|
||||
'scenario function can only be used as a decorator. Refer to the documentation.')
|
||||
|
||||
g.update(locals())
|
||||
|
@ -293,6 +227,8 @@ def scenario(
|
|||
_scenario = recreate_function(
|
||||
g[_pytestbdd_function.__name__], module=caller_module, firstlineno=caller_function.f_lineno)
|
||||
|
||||
params = scenario.get_params()
|
||||
|
||||
if params:
|
||||
_scenario = pytest.mark.parametrize(*params)(_scenario)
|
||||
|
||||
|
@ -300,6 +236,34 @@ def scenario(
|
|||
feature_name=feature_name, scenario_name=scenario_name)
|
||||
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
|
||||
SCENARIO_OUTLINE = 'scenario outline' # pragma: no cover
|
||||
EXAMPLES = 'examples' # pragma: no cover
|
||||
EXAMPLES_VERTICAL = 'examples vertical' # pragma: no cover
|
||||
EXAMPLES_HEADERS = 'example headers' # pragma: no cover
|
||||
EXAMPLE_LINE = 'example line' # pragma: no cover
|
||||
EXAMPLE_LINE_VERTICAL = 'example line vertical' # pragma: no cover
|
||||
SCENARIO = 'scenario' # pragma: no cover
|
||||
GIVEN = 'given' # pragma: no cover
|
||||
WHEN = 'when' # pragma: no cover
|
||||
|
|
|
@ -4,7 +4,7 @@ import functools
|
|||
import re
|
||||
import pytest
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.scenario import GivenAlreadyUsed
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
@scenario(
|
||||
|
@ -73,5 +73,5 @@ def test_multiple_given(request):
|
|||
)
|
||||
def test():
|
||||
pass
|
||||
with pytest.raises(GivenAlreadyUsed):
|
||||
with pytest.raises(exceptions.GivenAlreadyUsed):
|
||||
test(request)
|
||||
|
|
|
@ -28,3 +28,14 @@ Scenario Outline: Outlined with some examples failing
|
|||
| start | eat | left |
|
||||
| 0 | 5 | 5 |
|
||||
| 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
|
||||
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
from pytest_bdd.scenario import ScenarioExamplesNotValidError
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
@scenario(
|
||||
|
@ -13,7 +13,8 @@ from pytest_bdd.scenario import ScenarioExamplesNotValidError
|
|||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
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')
|
||||
|
@ -39,7 +40,7 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
|||
def test_wrongly_outlined(request):
|
||||
"""Test parametrized scenario when the test function lacks parameters."""
|
||||
|
||||
with pytest.raises(ScenarioExamplesNotValidError) as exc:
|
||||
with pytest.raises(exceptions.ScenarioExamplesNotValidError) as exc:
|
||||
@scenario(
|
||||
'outline.feature',
|
||||
'Outlined with wrong examples',
|
||||
|
@ -67,3 +68,14 @@ def other_fixture(request):
|
|||
)
|
||||
def test_outlined_with_other_fixtures(other_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
|
||||
|
||||
from pytest_bdd import scenario
|
||||
from pytest_bdd.scenario import ScenarioNotFound
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
def test_scenario_not_found(request):
|
||||
"""Test the situation when scenario is not found."""
|
||||
|
||||
with pytest.raises(ScenarioNotFound):
|
||||
with pytest.raises(exceptions.ScenarioNotFound):
|
||||
scenario(
|
||||
'not_found.feature',
|
||||
'NOT FOUND'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import pytest
|
||||
|
||||
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')
|
||||
|
@ -84,7 +84,7 @@ def test_multiple_given(request):
|
|||
def test():
|
||||
pass
|
||||
|
||||
with pytest.raises(GivenAlreadyUsed):
|
||||
with pytest.raises(exceptions.GivenAlreadyUsed):
|
||||
test(request)
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
"""Test wrong feature syntax."""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.feature import FeatureError
|
||||
from pytest_bdd.scenario import StepTypeError
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
@given('something')
|
||||
|
@ -57,10 +59,9 @@ def test_wrong_type_order(request, scenario_name):
|
|||
def test_wrong_type_order(request):
|
||||
pass
|
||||
|
||||
with pytest.raises(StepTypeError) as excinfo:
|
||||
with pytest.raises(exceptions.StepTypeError) as excinfo:
|
||||
test_wrong_type_order(request)
|
||||
|
||||
excinfo # TODO: assert the exception args from parameters
|
||||
assert re.match(r'Wrong step type \"(\w+)\" while \"(\w+)\" is expected\.', excinfo.value.args[0])
|
||||
|
||||
|
||||
def test_verbose_output(request):
|
||||
|
|
Loading…
Reference in New Issue