forked from test_framework/pytest-bdd
Merge pull request #36 from paylogic/scenario-outlines
scenario outlines implemented
This commit is contained in:
commit
f20c02a571
|
@ -1,6 +1,11 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
1.0.0
|
||||
-----
|
||||
|
||||
- Implemented scenario outlines (bubenkoff)
|
||||
|
||||
|
||||
0.6.10
|
||||
-----
|
||||
|
|
53
README.rst
53
README.rst
|
@ -34,7 +34,7 @@ Example
|
|||
publish\_article.feature:
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
|
||||
Feature: Blog
|
||||
A site where you can publish your articles.
|
||||
|
||||
|
@ -166,23 +166,59 @@ Scenario parameters
|
|||
===================
|
||||
Scenario can accept `encoding` param to decode content of feature file in specific encoding. UTF-8 is default.
|
||||
|
||||
Step parameters
|
||||
===============
|
||||
Scenario outlines
|
||||
=================
|
||||
|
||||
Scenarios can be parametrized to cover few cases. In Gherkin the variable
|
||||
templates are written using corner braces as <somevalue>.
|
||||
`Scenario outlines <http://docs.behat.org/guides/1.gherkin.html#scenario-outlines>`_ are supported by pytest-bdd
|
||||
exactly as it's described in be behave docs.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Scenario: Parametrized given, when, thens
|
||||
Scenario Outline: Outlined given, when, thens
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
||||
Unlike other tools, pytest-bdd implements the scenario outline not in the
|
||||
feature files, but in the python code using pytest parametrization.
|
||||
Examples:
|
||||
| start | eat | left |
|
||||
| 12 | 5 | 7 |
|
||||
|
||||
|
||||
The code will look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
|
||||
|
||||
test_outlined = scenario(
|
||||
'outline.feature',
|
||||
'Outlined given, when, thens',
|
||||
)
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
def start_cucumbers(start):
|
||||
return dict(start=int(start))
|
||||
|
||||
|
||||
@when('I eat <eat> cucumbers')
|
||||
def eat_cucumbers(start_cucumbers, start, eat):
|
||||
start_cucumbers['eat'] = int(eat)
|
||||
|
||||
|
||||
@then('I should have <left> cucumbers')
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
assert int(start) - int(eat) == int(left)
|
||||
assert start_cucumbers['start'] == int(start)
|
||||
assert start_cucumbers['eat'] == int(eat)
|
||||
|
||||
It's also possible to parametrize the scenario on the python side. This is done using pytest parametrization.
|
||||
The reason for this is that it is very often that some simple pythonic type
|
||||
is needed in the parameters like a datetime or a dictionary, which makes it
|
||||
more difficult to express in the text files and preserve the correct format.
|
||||
|
@ -224,6 +260,9 @@ The code will look like:
|
|||
assert start_cucumbers['start'] == start
|
||||
assert start_cucumbers['eat'] == eat
|
||||
|
||||
The significant downside of this approach is inability to see the test table from the feature file.
|
||||
|
||||
|
||||
Test setup
|
||||
==========
|
||||
|
||||
|
@ -324,6 +363,7 @@ inherited. Fixtures can be reused with other names using given():
|
|||
|
||||
given('I have beautiful article', fixture='article')
|
||||
|
||||
|
||||
Reusing steps
|
||||
=============
|
||||
|
||||
|
@ -363,6 +403,7 @@ test\_common.py:
|
|||
There are no definitions of the steps in the test file. They were
|
||||
collected from the parent conftests.
|
||||
|
||||
|
||||
Feature file paths
|
||||
==================
|
||||
|
||||
|
|
|
@ -26,7 +26,10 @@ one line.
|
|||
import re # pragma: no cover
|
||||
import sys # pragma: no cover
|
||||
|
||||
from pytest_bdd.types import FEATURE, SCENARIO, GIVEN, WHEN, THEN # pragma: no cover
|
||||
from pytest_bdd.types import (
|
||||
FEATURE, SCENARIO, SCENARIO_OUTLINE, EXAMPLES, EXAMPLES_HEADERS, EXAMPLE_LINE, GIVEN, WHEN,
|
||||
THEN # pragma: no cover
|
||||
)
|
||||
|
||||
|
||||
class FeatureError(Exception): # pragma: no cover
|
||||
|
@ -47,6 +50,8 @@ features = {} # pragma: no cover
|
|||
|
||||
STEP_PREFIXES = { # pragma: no cover
|
||||
'Feature: ': FEATURE,
|
||||
'Scenario Outline: ': SCENARIO_OUTLINE,
|
||||
'Examples:': EXAMPLES,
|
||||
'Scenario: ': SCENARIO,
|
||||
'Given ': GIVEN,
|
||||
'When ': WHEN,
|
||||
|
@ -137,6 +142,7 @@ class Feature(object):
|
|||
"""
|
||||
self.scenarios = {}
|
||||
|
||||
self.filename = filename
|
||||
scenario = None
|
||||
mode = None
|
||||
prev_mode = None
|
||||
|
@ -144,18 +150,17 @@ class Feature(object):
|
|||
|
||||
with _open_file(filename, encoding) as f:
|
||||
content = force_unicode(f.read(), encoding)
|
||||
for line_number, line in enumerate(content.split('\n')):
|
||||
for line_number, line in enumerate(content.splitlines()):
|
||||
line = strip(line)
|
||||
if not line:
|
||||
continue
|
||||
|
||||
mode = get_step_type(line) or mode
|
||||
|
||||
if mode == GIVEN and prev_mode not in (GIVEN, SCENARIO):
|
||||
if mode == GIVEN and prev_mode not in (GIVEN, SCENARIO, 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, GIVEN, WHEN):
|
||||
if mode == WHEN and prev_mode not in (SCENARIO, SCENARIO_OUTLINE, GIVEN, WHEN):
|
||||
raise FeatureError('When steps must be the first or follow Given steps',
|
||||
line_number, line)
|
||||
|
||||
|
@ -173,9 +178,15 @@ class Feature(object):
|
|||
|
||||
# Remove Feature, Given, When, Then, And
|
||||
line = remove_prefix(line)
|
||||
|
||||
if mode == SCENARIO:
|
||||
if mode in [SCENARIO, SCENARIO_OUTLINE]:
|
||||
self.scenarios[line] = scenario = Scenario(line)
|
||||
elif mode == EXAMPLES:
|
||||
mode = EXAMPLES_HEADERS
|
||||
elif mode == EXAMPLES_HEADERS:
|
||||
scenario.set_param_names([l.strip() for l in line.split('|') if l.strip()])
|
||||
mode = EXAMPLE_LINE
|
||||
elif mode == EXAMPLE_LINE:
|
||||
scenario.add_example([l.strip() for l in line.split('|') if l.strip()])
|
||||
elif mode and mode != FEATURE:
|
||||
scenario.add_step(step_name=line, step_type=mode)
|
||||
|
||||
|
@ -208,6 +219,8 @@ class Scenario(object):
|
|||
self.name = name
|
||||
self.params = set()
|
||||
self.steps = []
|
||||
self.example_params = []
|
||||
self.examples = []
|
||||
|
||||
def add_step(self, step_name, step_type):
|
||||
"""Add step to the scenario.
|
||||
|
@ -219,6 +232,23 @@ class Scenario(object):
|
|||
self.params.update(get_step_params(step_name))
|
||||
self.steps.append(Step(name=step_name, type=step_type))
|
||||
|
||||
def set_param_names(self, keys):
|
||||
"""Set parameter names.
|
||||
|
||||
:param names: `list` of `string` parameter names
|
||||
|
||||
"""
|
||||
self.params.update(keys)
|
||||
self.example_params = keys
|
||||
|
||||
def add_example(self, values):
|
||||
"""Add example.
|
||||
|
||||
:param values: `list` of `string` parameter values
|
||||
|
||||
"""
|
||||
self.examples.append(values)
|
||||
|
||||
|
||||
class Step(object):
|
||||
"""Step."""
|
||||
|
|
|
@ -41,6 +41,45 @@ 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.
|
||||
|
||||
:param request: pytest fixture request
|
||||
:param arg: argument name
|
||||
:param value: argument value
|
||||
|
||||
"""
|
||||
fd = python.FixtureDef(
|
||||
request._fixturemanager,
|
||||
None,
|
||||
arg,
|
||||
lambda: value, None, None,
|
||||
False,
|
||||
)
|
||||
fd.cached_result = (value, 0)
|
||||
|
||||
old_fd = getattr(request, '_fixturedefs', {}).get(arg)
|
||||
old_value = request._funcargs.get(arg)
|
||||
add_fixturename = arg not in request.fixturenames
|
||||
|
||||
def fin():
|
||||
request._fixturemanager._arg2fixturedefs[arg].remove(fd)
|
||||
getattr(request, '_fixturedefs', {})[arg] = old_fd
|
||||
request._funcargs[arg] = old_value
|
||||
if add_fixturename:
|
||||
request.fixturenames.remove(arg)
|
||||
|
||||
request.addfinalizer(fin)
|
||||
|
||||
# inject fixture definition
|
||||
request._fixturemanager._arg2fixturedefs.setdefault(arg, []).insert(0, fd)
|
||||
# inject fixture value in request cache
|
||||
getattr(request, '_fixturedefs', {})[arg] = fd
|
||||
request._funcargs[arg] = value
|
||||
if add_fixturename:
|
||||
request.fixturenames.append(arg)
|
||||
|
||||
|
||||
def _find_step_function(request, name, encoding):
|
||||
"""Match the step defined by the regular expression pattern.
|
||||
|
||||
|
@ -63,34 +102,88 @@ def _find_step_function(request, name, encoding):
|
|||
|
||||
if match:
|
||||
for arg, value in match.groupdict().items():
|
||||
fd = python.FixtureDef(
|
||||
request._fixturemanager,
|
||||
fixturedef.baseid,
|
||||
arg,
|
||||
lambda: value, fixturedef.scope, fixturedef.params,
|
||||
fixturedef.unittest,
|
||||
)
|
||||
fd.cached_result = (value, 0)
|
||||
|
||||
old_fd = getattr(request, '_fixturedefs', {}).get(arg)
|
||||
old_value = request._funcargs.get(arg)
|
||||
|
||||
def fin():
|
||||
request._fixturemanager._arg2fixturedefs[arg].remove(fd)
|
||||
getattr(request, '_fixturedefs', {})[arg] = old_fd
|
||||
request._funcargs[arg] = old_value
|
||||
|
||||
request.addfinalizer(fin)
|
||||
|
||||
# inject fixture definition
|
||||
request._fixturemanager._arg2fixturedefs.setdefault(arg, []).insert(0, fd)
|
||||
# inject fixture value in request cache
|
||||
getattr(request, '_fixturedefs', {})[arg] = fd
|
||||
request._funcargs[arg] = value
|
||||
_inject_fixture(request, arg, value)
|
||||
return request.getfuncargvalue(pattern.pattern)
|
||||
raise
|
||||
|
||||
|
||||
def _validate_scenario(feature, scenario, request):
|
||||
"""Validate the scenario."""
|
||||
resolved_params = scenario.params.intersection(request.fixturenames)
|
||||
|
||||
if scenario.params != resolved_params:
|
||||
raise NotEnoughScenarioParams(
|
||||
"""Scenario "{0}" in the feature "{1}" was not able to resolve all declared parameters."""
|
||||
"""Should resolve params: {2}, but resolved only: {3}.""".format(
|
||||
scenario.name, feature.filename, sorted(scenario.params), sorted(resolved_params),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _execute_scenario_outline(feature, scenario, request, encoding):
|
||||
"""Execute the scenario outline."""
|
||||
for example in scenario.examples:
|
||||
for key, value in dict(zip(scenario.example_params, example)).items():
|
||||
_inject_fixture(request, key, value)
|
||||
_execute_scenario(feature, scenario, request, encoding)
|
||||
|
||||
|
||||
def _execute_scenario(feature, scenario, request, encoding):
|
||||
"""Execute the scenario."""
|
||||
|
||||
_validate_scenario(feature, scenario, request)
|
||||
|
||||
givens = set()
|
||||
# Execute scenario steps
|
||||
for step in scenario.steps:
|
||||
try:
|
||||
step_func = _find_step_function(request, step.name, encoding=encoding)
|
||||
except python.FixtureLookupError as exception:
|
||||
request.config.hook.pytest_bdd_step_func_lookup_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, exception=exception)
|
||||
raise
|
||||
|
||||
try:
|
||||
# Check the step types are called in the correct order
|
||||
if step_func.step_type != step.type:
|
||||
raise 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(
|
||||
'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:
|
||||
request.config.hook.pytest_bdd_step_validation_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
exception=exception)
|
||||
raise
|
||||
|
||||
kwargs = {}
|
||||
try:
|
||||
# Get the step argument values
|
||||
kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(step_func).args)
|
||||
request.config.hook.pytest_bdd_before_step(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
step_func_args=kwargs)
|
||||
# Execute the step
|
||||
step_func(**kwargs)
|
||||
request.config.hook.pytest_bdd_after_step(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
step_func_args=kwargs)
|
||||
except Exception as exception:
|
||||
request.config.hook.pytest_bdd_step_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
step_func_args=kwargs, exception=exception)
|
||||
raise
|
||||
|
||||
|
||||
def scenario(feature_name, scenario_name, encoding='utf-8'):
|
||||
"""Scenario. May be called both as decorator and as just normal function."""
|
||||
|
||||
|
@ -113,65 +206,10 @@ def scenario(feature_name, scenario_name, encoding='utf-8'):
|
|||
'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name)
|
||||
)
|
||||
|
||||
resolved_params = scenario.params.intersection(request.fixturenames)
|
||||
|
||||
if scenario.params != resolved_params:
|
||||
raise NotEnoughScenarioParams(
|
||||
"""Scenario "{0}" in the feature "{1}" was not able to resolve all declared parameters."""
|
||||
"""Should resolve params: {2}, but resolved only: {3}.""".format(
|
||||
scenario_name, feature_name, sorted(scenario.params), sorted(resolved_params),
|
||||
)
|
||||
)
|
||||
|
||||
givens = set()
|
||||
# Execute scenario steps
|
||||
for step in scenario.steps:
|
||||
try:
|
||||
step_func = _find_step_function(request, step.name, encoding=encoding)
|
||||
except python.FixtureLookupError as exception:
|
||||
request.config.hook.pytest_bdd_step_func_lookup_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, exception=exception)
|
||||
raise
|
||||
|
||||
try:
|
||||
# Check the step types are called in the correct order
|
||||
if step_func.step_type != step.type:
|
||||
raise 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(
|
||||
'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:
|
||||
request.config.hook.pytest_bdd_step_validation_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
exception=exception)
|
||||
raise
|
||||
|
||||
kwargs = {}
|
||||
try:
|
||||
# Get the step argument values
|
||||
kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(step_func).args)
|
||||
request.config.hook.pytest_bdd_before_step(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
step_func_args=kwargs)
|
||||
# Execute the step
|
||||
step_func(**kwargs)
|
||||
request.config.hook.pytest_bdd_after_step(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
step_func_args=kwargs)
|
||||
except Exception as exception:
|
||||
request.config.hook.pytest_bdd_step_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
step_func_args=kwargs, exception=exception)
|
||||
raise
|
||||
if scenario.examples:
|
||||
_execute_scenario_outline(feature, scenario, request, encoding)
|
||||
else:
|
||||
_execute_scenario(feature, scenario, request, encoding)
|
||||
|
||||
_scenario.pytestbdd_params = set()
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"""Common type definitions."""
|
||||
|
||||
FEATURE = 'feature' # pragma: no cover
|
||||
SCENARIO_OUTLINE = 'scenario outline' # pragma: no cover
|
||||
EXAMPLES = 'examples' # pragma: no cover
|
||||
EXAMPLES_HEADERS = 'example headers' # pragma: no cover
|
||||
EXAMPLE_LINE = 'example line' # pragma: no cover
|
||||
SCENARIO = 'scenario' # pragma: no cover
|
||||
GIVEN = 'given' # pragma: no cover
|
||||
WHEN = 'when' # pragma: no cover
|
||||
|
|
2
setup.py
2
setup.py
|
@ -6,7 +6,7 @@ from setuptools import setup
|
|||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
version = '0.6.10'
|
||||
version = '1.0.0'
|
||||
|
||||
|
||||
class Tox(TestCommand):
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
Scenario Outline: Outlined given, when, thens
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
||||
Examples:
|
||||
| start | eat | left |
|
||||
| 12 | 5 | 7 |
|
|
@ -0,0 +1,25 @@
|
|||
"""Scenario Outline tests."""
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
|
||||
|
||||
test_outlined = scenario(
|
||||
'outline.feature',
|
||||
'Outlined given, when, thens',
|
||||
)
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
def start_cucumbers(start):
|
||||
return dict(start=int(start))
|
||||
|
||||
|
||||
@when('I eat <eat> cucumbers')
|
||||
def eat_cucumbers(start_cucumbers, start, eat):
|
||||
start_cucumbers['eat'] = int(eat)
|
||||
|
||||
|
||||
@then('I should have <left> cucumbers')
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
assert int(start) - int(eat) == int(left)
|
||||
assert start_cucumbers['start'] == int(start)
|
||||
assert start_cucumbers['eat'] == int(eat)
|
|
@ -16,6 +16,22 @@ def test_parametrized(request, start, eat, left):
|
|||
"""Test parametrized scenario."""
|
||||
|
||||
|
||||
@pytest.fixture(params=[1, 2])
|
||||
def foo_bar(request):
|
||||
return 'foo_bar' * request.param
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
['start', 'eat', 'left'],
|
||||
[(12, 5, 7)])
|
||||
@scenario(
|
||||
'parametrized.feature',
|
||||
'Parametrized given, when, thens',
|
||||
)
|
||||
def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar):
|
||||
"""Test parametrized scenario, but also with other fixtures."""
|
||||
|
||||
|
||||
def test_parametrized_wrongly(request):
|
||||
"""Test parametrized scenario when the test function lacks parameters."""
|
||||
@scenario(
|
||||
|
|
Loading…
Reference in New Issue