Scenario outline implementation based on pure pytest parametrization

This commit is contained in:
Anatoly Bubenkov 2014-03-11 15:05:25 +01:00
parent f20c02a571
commit bca5206677
17 changed files with 314 additions and 174 deletions

View File

@ -1,6 +1,13 @@
Changelog Changelog
========= =========
2.0.0
-----
- Pure pytest parametrization for scenario outlines (bubenkoff)
- Splitting scenario decorated and non-decorated variants (bubenkoff)
1.0.0 1.0.0
----- -----

View File

@ -21,6 +21,7 @@ mentioned in the feature steps with dependency injection, which allows a true BD
just-enough specification of the requirements without maintaining any context object just-enough specification of the requirements without maintaining any context object
containing the side effects of the Gherkin imperative declarations. containing the side effects of the Gherkin imperative declarations.
Install pytest-bdd Install pytest-bdd
================== ==================
@ -28,6 +29,7 @@ Install pytest-bdd
pip install pytest-bdd pip install pytest-bdd
Example Example
======= =======
@ -81,6 +83,7 @@ test\_publish\_article.py:
article.refresh() # Refresh the object in the SQLAlchemy session article.refresh() # Refresh the object in the SQLAlchemy session
assert article.is_published assert article.is_published
Step aliases Step aliases
============ ============
@ -113,6 +116,7 @@ default author.
Given I'm the admin Given I'm the admin
And there is an article And there is an article
Step arguments Step arguments
============== ==============
@ -143,28 +147,32 @@ The code will look like:
test_arguments = scenario('arguments.feature', 'Arguments for given, when, thens') test_arguments = scenario('arguments.feature', 'Arguments for given, when, thens')
@given(re.compile('there are (?P<start>\d+) cucumbers')) @given(re.compile('there are (?P<start>\d+) cucumbers'), converters=dict(start=int))
def start_cucumbers(start): def start_cucumbers(start):
# note that you always get step arguments as strings, convert them on demand
start = int(start)
return dict(start=start, eat=0) return dict(start=start, eat=0)
@when(re.compile('I eat (?P<eat>\d+) cucumbers')) @when(re.compile('I eat (?P<eat>\d+) cucumbers'), converters=dict(eat=int))
def eat_cucumbers(start_cucumbers, eat): def eat_cucumbers(start_cucumbers, eat):
eat = int(eat)
start_cucumbers['eat'] += eat start_cucumbers['eat'] += eat
@then(re.compile('I should have (?P<left>\d+) cucumbers')) @then(re.compile('I should have (?P<left>\d+) cucumbers'), converters=dict(left=int))
def should_have_left_cucumbers(start_cucumbers, start, left): def should_have_left_cucumbers(start_cucumbers, start, left):
start, left = int(start), int(left)
assert start_cucumbers['start'] == start assert start_cucumbers['start'] == start
assert start - start_cucumbers['eat'] == left assert start - start_cucumbers['eat'] == left
Example code also shows possibility to pass argument converters which may be useful if you need argument types
different than strings.
Scenario parameters Scenario parameters
=================== ===================
Scenario can accept `encoding` param to decode content of feature file in specific encoding. UTF-8 is default. Scenario function/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 Scenario outlines
================= =================
@ -174,7 +182,6 @@ 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 `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. exactly as it's described in be behave docs.
Example: Example:
.. code-block:: feature .. code-block:: feature
@ -199,42 +206,47 @@ The code will look like:
test_outlined = scenario( test_outlined = scenario(
'outline.feature', 'outline.feature',
'Outlined given, when, thens', 'Outlined given, when, thens',
example_converters=dict(start=int, eat=float, left=str)
) )
@given('there are <start> cucumbers') @given('there are <start> cucumbers')
def start_cucumbers(start): def start_cucumbers(start):
return dict(start=int(start)) assert isinstance(start, int)
return dict(start=start)
@when('I eat <eat> cucumbers') @when('I eat <eat> cucumbers')
def eat_cucumbers(start_cucumbers, start, eat): def eat_cucumbers(start_cucumbers, eat):
start_cucumbers['eat'] = int(eat) assert isinstance(eat, float)
start_cucumbers['eat'] = eat
@then('I should have <left> cucumbers') @then('I should have <left> cucumbers')
def should_have_left_cucumbers(start_cucumbers, start, eat, left): def should_have_left_cucumbers(start_cucumbers, start, eat, left):
assert int(start) - int(eat) == int(left) assert isinstance(left, str)
assert start_cucumbers['start'] == int(start) assert start - eat == int(left)
assert start_cucumbers['eat'] == int(eat) assert start_cucumbers['start'] == start
assert start_cucumbers['eat'] == eat
It's also possible to parametrize the scenario on the python side. This is done using pytest parametrization. Example code also shows possibility to pass example converters which may be useful if you need parameter types
The reason for this is that it is very often that some simple pythonic type different than strings.
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. It's also possible to parametrize the scenario on the python side.
The reason for this is that it is sometimes not needed to mention example table for every scenario.
The code will look like: The code will look like:
.. code-block:: python .. code-block:: python
import pytest import pytest
from pytest_bdd import scenario, given, when, then from pytest_bdd import 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'],
[(12, 5, 7)]) [(12, 5, 7)])
@scenario( @mark.scenario(
'parametrized.feature', 'parametrized.feature',
'Parametrized given, when, thens', 'Parametrized given, when, thens',
) )
@ -453,6 +465,7 @@ test\_publish\_article.py:
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
===== =====
@ -474,15 +487,13 @@ which might be helpful building useful reporting, visualization, etc on top of i
* pytest_bdd_step_func_lookup_error(request, feature, scenario, step, exception) - Called when step lookup failed * pytest_bdd_step_func_lookup_error(request, feature, scenario, step, exception) - Called when step lookup failed
Subplugins Browser testing
========== ===============
The pytest BDD has plugin support, and the main purpose of plugins Tools recommended to use for browser testing:
(subplugins) is to provide useful and specialized fixtures.
List of known subplugins: * pytest-splinter - pytest splinter integration for the real browser testing
* pytest-bdd-splinter - collection of fixtures for the real browser BDD testing
License License
======= =======

View File

@ -1,5 +1,5 @@
from pytest_bdd.steps import given, when, then # pragma: no cover from pytest_bdd.steps import given, when, then # pragma: no cover
from pytest_bdd.scenario import scenario # pragma: no cover from pytest_bdd.scenario import scenario # pragma: no cover
from pytest_bdd import mark # pragma: no cover
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, mark.__name__] # pragma: no cover
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__] # pragma: no cover

View File

@ -229,8 +229,9 @@ class Scenario(object):
:param step_type: Step type. :param step_type: Step type.
""" """
self.params.update(get_step_params(step_name)) params = get_step_params(step_name)
self.steps.append(Step(name=step_name, type=step_type)) self.params.update(params)
self.steps.append(Step(name=step_name, type=step_type, params=params))
def set_param_names(self, keys): def set_param_names(self, keys):
"""Set parameter names. """Set parameter names.
@ -238,8 +239,7 @@ class Scenario(object):
:param names: `list` of `string` parameter names :param names: `list` of `string` parameter names
""" """
self.params.update(keys) self.example_params = [str(key) for key in keys]
self.example_params = keys
def add_example(self, values): def add_example(self, values):
"""Add example. """Add example.
@ -253,6 +253,7 @@ class Scenario(object):
class Step(object): class Step(object):
"""Step.""" """Step."""
def __init__(self, name, type): def __init__(self, name, type, params):
self.name = name self.name = name
self.type = type self.type = type
self.params = params

26
pytest_bdd/mark.py Normal file
View File

@ -0,0 +1,26 @@
"""Pytest-bdd markers."""
import inspect
from pytest_bdd.steps import recreate_function, get_caller_module, get_caller_function
from pytest_bdd import scenario as bdd_scenario
def scenario(feature_name, scenario_name, encoding='utf-8', example_converters=None):
"""Scenario. May be called both as decorator and as just normal function."""
caller_module = get_caller_module()
caller_function = get_caller_function()
def decorator(request):
_scenario = bdd_scenario(
feature_name, scenario_name, encoding=encoding, example_converters=example_converters,
caller_module=caller_module, caller_function=caller_function)
args = inspect.getargspec(request).args
_scenario = recreate_function(_scenario, name=request.__name__, module=caller_module, add_args=args)
return _scenario
return recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno)

View File

@ -11,15 +11,26 @@ test_publish_article = scenario(
) )
""" """
import collections
import os
import imp
import sys
import inspect # pragma: no cover import inspect # pragma: no cover
from os import path as op # pragma: no cover from os import path as op # pragma: no cover
import pytest
from future import utils as future_utils
from _pytest import python 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 recreate_function, get_caller_module, get_caller_function from pytest_bdd.steps import recreate_function, get_caller_module, get_caller_function
from pytest_bdd.types import GIVEN from pytest_bdd.types import GIVEN
from pytest_bdd import plugin
class ScenarioValidationError(Exception): class ScenarioValidationError(Exception):
"""Base class for scenario validation.""" """Base class for scenario validation."""
@ -29,8 +40,8 @@ class ScenarioNotFound(ScenarioValidationError): # pragma: no cover
"""Scenario Not Found""" """Scenario Not Found"""
class NotEnoughScenarioParams(ScenarioValidationError): # pragma: no cover class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover
"""Scenario function doesn't take enough parameters in the arguments.""" """Scenario steps argumets do not match declared scenario examples."""
class StepTypeError(ScenarioValidationError): # pragma: no cover class StepTypeError(ScenarioValidationError): # pragma: no cover
@ -101,7 +112,10 @@ def _find_step_function(request, name, encoding):
match = pattern.match(name) if pattern else None match = pattern.match(name) if pattern else None
if match: if match:
converters = getattr(fixturedef.func, 'converters', {})
for arg, value in match.groupdict().items(): for arg, value in match.groupdict().items():
if arg in converters:
value = converters[arg](value)
_inject_fixture(request, arg, value) _inject_fixture(request, arg, value)
return request.getfuncargvalue(pattern.pattern) return request.getfuncargvalue(pattern.pattern)
raise raise
@ -109,26 +123,58 @@ def _find_step_function(request, name, encoding):
def _validate_scenario(feature, scenario, request): def _validate_scenario(feature, scenario, request):
"""Validate the scenario.""" """Validate the scenario."""
resolved_params = scenario.params.intersection(request.fixturenames) if scenario.params and scenario.example_params and scenario.params != set(scenario.example_params):
raise ScenarioExamplesNotValidError(
if scenario.params != resolved_params: """Scenario "{0}" in the feature "{1}" has not valid examples. """
raise NotEnoughScenarioParams( """Set of step parameters {2} should match set of example values {3}.""".format(
"""Scenario "{0}" in the feature "{1}" was not able to resolve all declared parameters.""" scenario.name, feature.filename, sorted(scenario.params), sorted(scenario.example_params),
"""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): def _execute_scenario_outline(feature, scenario, request, encoding, example_converters=None):
"""Execute the scenario outline.""" """Execute the scenario outline."""
errors = []
# tricky part, basically here we clear pytest request cache
for example in scenario.examples: for example in scenario.examples:
for key, value in dict(zip(scenario.example_params, example)).items(): request._funcargs = {}
request._arg2index = {}
try:
_execute_scenario(feature, scenario, request, encoding, example=dict(zip(scenario.example_params, example)))
except Exception as e:
errors.append([e, sys.exc_info()[2]])
for error in errors:
raise future_utils.raise_with_traceback(error[0], error[1])
def _execute_step_function(request, feature, step, step_func, example=None):
"""Execute step function."""
kwargs = {}
if example:
for key in step.params:
value = example[key]
if step_func.converters and key in step_func.converters:
value = step_func.converters[key](value)
_inject_fixture(request, key, value) _inject_fixture(request, key, value)
_execute_scenario(feature, scenario, request, encoding) 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 _execute_scenario(feature, scenario, request, encoding): def _execute_scenario(feature, scenario, request, encoding, example=None):
"""Execute the scenario.""" """Execute the scenario."""
_validate_scenario(feature, scenario, request) _validate_scenario(feature, scenario, request)
@ -165,68 +211,77 @@ def _execute_scenario(feature, scenario, request, encoding):
exception=exception) exception=exception)
raise raise
kwargs = {} _execute_step_function(request, feature, step, step_func, example=example)
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'): FakeRequest = collections.namedtuple('FakeRequest', ['module'])
"""Scenario. May be called both as decorator and as just normal function."""
caller_module = get_caller_module()
caller_function = get_caller_function()
def decorator(request): def get_fixture(caller_module, fixture, path=None, module=None):
"""Get first conftest module from given one."""
def call_fixture(function):
args = []
if 'request' in inspect.getargspec(function).args:
args = [FakeRequest(module=caller_module)]
return function(*args)
def _scenario(request): if not module:
# Get the feature module = caller_module
base_path = request.getfuncargvalue('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 if hasattr(module, fixture):
try: return call_fixture(getattr(module, fixture))
scenario = feature.scenarios[scenario_name]
except KeyError:
raise ScenarioNotFound(
'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name)
)
if scenario.examples: if path is None:
_execute_scenario_outline(feature, scenario, request, encoding) path = os.path.dirname(module.__file__)
else: if os.path.exists(os.path.join(path, '__init__.py')):
_execute_scenario(feature, scenario, request, encoding) file_path = os.path.join(path, 'conftest.py')
if os.path.exists(file_path):
conftest = imp.load_source('conftest', file_path)
if hasattr(conftest, fixture):
return get_fixture(caller_module, fixture, module=conftest)
else:
return get_fixture(caller_module, fixture, module=plugin)
return get_fixture(caller_module, fixture, path=os.path.dirname(path), module=module)
_scenario.pytestbdd_params = set()
if isinstance(request, python.FixtureRequest): def scenario(
# Called as a normal function. feature_name, scenario_name, encoding='utf-8', example_converters=None,
_scenario = recreate_function(_scenario, module=caller_module) caller_module=None, caller_function=None):
return _scenario(request) """Scenario."""
# Used as a decorator. Modify the returned function to add parameters from a decorated function. caller_module = caller_module or get_caller_module()
func_args = inspect.getargspec(request).args caller_function = caller_function or get_caller_function()
if 'request' in func_args:
func_args.remove('request')
_scenario = recreate_function(_scenario, name=request.__name__, add_args=func_args, module=caller_module)
_scenario.pytestbdd_params = set(func_args)
return _scenario # 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)
decorator = recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) # 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)
)
return decorator 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 = []
def _scenario(request, *args, **kwargs):
_execute_scenario(feature, scenario, request, encoding)
_scenario = recreate_function(
_scenario, module=caller_module, firstlineno=caller_function.f_lineno,
add_args=scenario.example_params)
if params:
_scenario = pytest.mark.parametrize(*params)(_scenario)
return _scenario

View File

@ -51,11 +51,13 @@ class StepError(Exception): # pragma: no cover
RE_TYPE = type(re.compile('')) # pragma: no cover RE_TYPE = type(re.compile('')) # pragma: no cover
def given(name, fixture=None): def given(name, fixture=None, converters=None):
"""Given step decorator. """Given step decorator.
:param name: Given step name. :param name: Given step name.
:param fixture: Optional name of the fixture to reuse. :param fixture: Optional name of the fixture to reuse.
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:raises: StepError in case of wrong configuration. :raises: StepError in case of wrong configuration.
:note: Can't be used as a decorator when the fixture is specified. :note: Can't be used as a decorator when the fixture is specified.
@ -66,6 +68,7 @@ def given(name, fixture=None):
module = get_caller_module() module = get_caller_module()
step_func = lambda request: request.getfuncargvalue(fixture) step_func = lambda request: request.getfuncargvalue(fixture)
step_func.step_type = GIVEN step_func.step_type = GIVEN
step_func.converters = converters
step_func.__name__ = name step_func.__name__ = name
step_func.fixture = fixture step_func.fixture = fixture
func = pytest.fixture(lambda: step_func) func = pytest.fixture(lambda: step_func)
@ -73,29 +76,33 @@ def given(name, fixture=None):
contribute_to_module(module, remove_prefix(name), func) contribute_to_module(module, remove_prefix(name), func)
return _not_a_fixture_decorator return _not_a_fixture_decorator
return _step_decorator(GIVEN, name) return _step_decorator(GIVEN, name, converters=converters)
def when(name): def when(name, converters=None):
"""When step decorator. """When step decorator.
:param name: Step name. :param name: Step name.
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:raises: StepError in case of wrong configuration. :raises: StepError in case of wrong configuration.
""" """
return _step_decorator(WHEN, name) return _step_decorator(WHEN, name, converters=converters)
def then(name): def then(name, converters=None):
"""Then step decorator. """Then step decorator.
:param name: Step name. :param name: Step name.
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:raises: StepError in case of wrong configuration. :raises: StepError in case of wrong configuration.
""" """
return _step_decorator(THEN, name) return _step_decorator(THEN, name, converters=converters)
def _not_a_fixture_decorator(func): def _not_a_fixture_decorator(func):
@ -109,7 +116,7 @@ def _not_a_fixture_decorator(func):
raise StepError('Cannot be used as a decorator when the fixture is specified') raise StepError('Cannot be used as a decorator when the fixture is specified')
def _step_decorator(step_type, step_name): def _step_decorator(step_type, step_name, converters=None):
"""Step decorator for the type and the name. """Step decorator for the type and the name.
:param step_type: Step type (GIVEN, WHEN or THEN). :param step_type: Step type (GIVEN, WHEN or THEN).
@ -141,6 +148,7 @@ def _step_decorator(step_type, step_name):
step_func.__name__ = step_name step_func.__name__ = step_name
step_func.step_type = step_type step_func.step_type = step_type
step_func.converters = converters
@pytest.fixture @pytest.fixture
def lazy_step_func(): def lazy_step_func():
@ -151,6 +159,8 @@ def _step_decorator(step_type, step_name):
if pattern: if pattern:
lazy_step_func.pattern = pattern lazy_step_func.pattern = pattern
if converters:
lazy_step_func.converters = converters
contribute_to_module( contribute_to_module(
get_caller_module(), get_caller_module(),
@ -162,7 +172,7 @@ def _step_decorator(step_type, step_name):
return decorator return decorator
def recreate_function(func, module=None, name=None, add_args=(), firstlineno=None): def recreate_function(func, module=None, name=None, add_args=[], firstlineno=None):
"""Recreate a function, replacing some info. """Recreate a function, replacing some info.
:param func: Function object. :param func: Function object.
@ -188,6 +198,10 @@ def recreate_function(func, module=None, name=None, add_args=(), firstlineno=Non
if PY3: if PY3:
argnames.insert(1, 'co_kwonlyargcount') argnames.insert(1, 'co_kwonlyargcount')
for arg in inspect.getargspec(func).args:
if arg in add_args:
add_args.remove(arg)
args = [] args = []
code = get_code(func) code = get_code(func)
for arg in argnames: for arg in argnames:

View File

@ -6,7 +6,7 @@ from setuptools import setup
from setuptools.command.test import test as TestCommand from setuptools.command.test import test as TestCommand
version = '1.0.0' version = '2.0.0'
class Tox(TestCommand): class Tox(TestCommand):
@ -54,6 +54,7 @@ setup(
cmdclass={'test': Tox}, cmdclass={'test': Tox},
install_requires=[ install_requires=[
'pytest', 'pytest',
'future'
], ],
# the following makes a plugin available to py.test # the following makes a plugin available to py.test
entry_points={ entry_points={

View File

@ -1,11 +1,11 @@
Scenario: Every step takes a parameter with the same name Scenario: Every step takes a parameter with the same name
Given I have 1 Euro Given I have 1 Euro
When I pay 2 Euro When I pay 2 Euro
And I pay 1 Euro And I pay 1 Euro
Then I should have 0 Euro Then I should have 0 Euro
And I should have 999999 Euro # In my dream... And I should have 999999 Euro # In my dream...
Scenario: Using the same given fixture raises an error Scenario: Using the same given fixture raises an error
Given I have 1 Euro Given I have 1 Euro
And I have 2 Euro And I have 2 Euro

View File

@ -19,19 +19,24 @@ test_argument_in_when_step_2 = sc('Argument in when, step 2')
@pytest.fixture @pytest.fixture
def values(): def values():
return ['1', '2', '1', '0', '999999'] return [1, 2, 1, 0, 999999]
@given(re.compile(r'I have (?P<euro>\d+) Euro')) @given(re.compile(r'I have (?P<euro>\d+) Euro'), converters=dict(euro=int))
def i_have(euro, values): def i_have(euro, values):
assert euro == values.pop(0) assert euro == values.pop(0)
@when(re.compile(r'I pay (?P<euro>\d+) Euro')) @when(re.compile(r'I pay (?P<euro>\d+) Euro'), converters=dict(euro=int))
def i_pay(euro, values, request): def i_pay(euro, values, request):
assert euro == values.pop(0) assert euro == values.pop(0)
@then(re.compile(r'I should have (?P<euro>\d+) Euro'), converters=dict(euro=int))
def i_should_have(euro, values):
assert euro == values.pop(0)
@given('I have an argument') @given('I have an argument')
def argument(): def argument():
"""I have an argument.""" """I have an argument."""
@ -44,11 +49,6 @@ def get_argument(argument, arg):
argument['arg'] = arg argument['arg'] = arg
@then(re.compile(r'I should have (?P<euro>\d+) Euro'))
def i_should_have(euro, values):
assert euro == values.pop(0)
@then(re.compile('My argument should be (?P<arg>\d+)')) @then(re.compile('My argument should be (?P<arg>\d+)'))
def assert_that_my_argument_is_arg(argument, arg): def assert_that_my_argument_is_arg(argument, arg):
"""Assert that arg from when equals arg.""" """Assert that arg from when equals arg."""

View File

@ -6,3 +6,24 @@ Scenario Outline: Outlined given, when, thens
Examples: Examples:
| start | eat | left | | start | eat | left |
| 12 | 5 | 7 | | 12 | 5 | 7 |
Scenario Outline: Outlined with wrong examples
Given there are <start> cucumbers
When I eat <eat> cucumbers
Then I should have <left> cucumbers
Examples:
| start | eat | left | unknown_param |
| 12 | 5 | 7 | value |
Scenario Outline: Outlined with some examples failing
Given there are <start> cucumbers
When I eat <eat> cucumbers
Then I should have <left> cucumbers
Examples:
| start | eat | left |
| 0 | 5 | 5 |
| 12 | 5 | 7 |

View File

@ -20,8 +20,6 @@ def pytestbdd_feature_base_dir():
def test_feature_path(request, scenario_name): def test_feature_path(request, scenario_name):
"""Test feature base dir.""" """Test feature base dir."""
sc = scenario('steps.feature', scenario_name)
with pytest.raises(IOError) as exc: with pytest.raises(IOError) as exc:
sc(request) scenario('steps.feature', scenario_name)
assert os.path.join('/does/not/exist/', 'steps.feature') in str(exc.value) assert os.path.join('/does/not/exist/', 'steps.feature') in str(exc.value)

View File

@ -1,25 +1,55 @@
"""Scenario Outline tests.""" """Scenario Outline tests."""
import re
import pytest
from pytest_bdd import given, when, then, scenario from pytest_bdd import given, when, then, scenario
from pytest_bdd import mark
from pytest_bdd.scenario import ScenarioExamplesNotValidError
test_outlined = scenario( test_outlined = scenario(
'outline.feature', 'outline.feature',
'Outlined given, when, thens', 'Outlined given, when, thens',
example_converters=dict(start=int, eat=float, left=str)
) )
@given('there are <start> cucumbers') @given('there are <start> cucumbers')
def start_cucumbers(start): def start_cucumbers(start):
return dict(start=int(start)) assert isinstance(start, int)
return dict(start=start)
@when('I eat <eat> cucumbers') @when('I eat <eat> cucumbers')
def eat_cucumbers(start_cucumbers, start, eat): def eat_cucumbers(start_cucumbers, eat):
start_cucumbers['eat'] = int(eat) assert isinstance(eat, float)
start_cucumbers['eat'] = eat
@then('I should have <left> cucumbers') @then('I should have <left> cucumbers')
def should_have_left_cucumbers(start_cucumbers, start, eat, left): def should_have_left_cucumbers(start_cucumbers, start, eat, left):
assert int(start) - int(eat) == int(left) assert isinstance(left, str)
assert start_cucumbers['start'] == int(start) assert start - eat == int(left)
assert start_cucumbers['eat'] == int(eat) assert start_cucumbers['start'] == start
assert start_cucumbers['eat'] == eat
def test_wrongly_outlined(request):
"""Test parametrized scenario when the test function lacks parameters."""
@mark.scenario(
'outline.feature',
'Outlined with wrong examples',
)
def wrongly_outlined(request):
pass
with pytest.raises(ScenarioExamplesNotValidError) as exc:
wrongly_outlined(request, 1, 2, 3, 4)
assert re.match(
"""Scenario \"Outlined with wrong examples\" in the feature \"(.+)\" has not valid examples\. """
"""Set of step parameters (.+) should match set of example values """
"""(.+)\.""",
exc.value.args[0]
)

View File

@ -1,14 +1,12 @@
import pytest import pytest
from pytest_bdd.scenario import NotEnoughScenarioParams from pytest_bdd import given, when, then, mark
from pytest_bdd import given, when, then, scenario
@pytest.mark.parametrize( @pytest.mark.parametrize(
['start', 'eat', 'left'], ['start', 'eat', 'left'],
[(12, 5, 7)]) [(12, 5, 7)])
@scenario( @mark.scenario(
'parametrized.feature', 'parametrized.feature',
'Parametrized given, when, thens', 'Parametrized given, when, thens',
) )
@ -18,37 +16,18 @@ def test_parametrized(request, start, eat, left):
@pytest.fixture(params=[1, 2]) @pytest.fixture(params=[1, 2])
def foo_bar(request): def foo_bar(request):
return 'foo_bar' * request.param return 'bar' * request.param
@pytest.mark.parametrize( @pytest.mark.parametrize(
['start', 'eat', 'left'], ['start', 'eat', 'left'],
[(12, 5, 7)]) [(12, 5, 7)])
@scenario( @mark.scenario(
'parametrized.feature', 'parametrized.feature',
'Parametrized given, when, thens', 'Parametrized given, when, thens',
) )
def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar): def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar):
"""Test parametrized scenario, but also with other fixtures.""" """Test parametrized scenario, but also with other parametrized fixtures."""
def test_parametrized_wrongly(request):
"""Test parametrized scenario when the test function lacks parameters."""
@scenario(
'parametrized.feature',
'Parametrized given, when, thens',
)
def wrongly_parametrized(request):
pass
with pytest.raises(NotEnoughScenarioParams) as exc:
wrongly_parametrized(request)
assert exc.value.args == (
"""Scenario "Parametrized given, when, thens" in the feature "parametrized.feature" was not able to """
"""resolve all declared parameters. """
"""Should resolve params: [\'eat\', \'left\', \'start\'], but resolved only: []."""
)
@given('there are <start> cucumbers') @given('there are <start> cucumbers')

View File

@ -6,10 +6,9 @@ from pytest_bdd.scenario import ScenarioNotFound
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."""
test_not_found = scenario(
'not_found.feature',
'NOT FOUND'
)
with pytest.raises(ScenarioNotFound): with pytest.raises(ScenarioNotFound):
test_not_found(request) scenario(
'not_found.feature',
'NOT FOUND'
)

View File

@ -96,7 +96,7 @@ def test_step_hooks(testdir):
""") """)
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
from pytest_bdd import given, when, scenario from pytest_bdd import given, when, mark
@given('I have a bar') @given('I have a bar')
def i_have_bar(): def i_have_bar():
@ -118,15 +118,15 @@ def test_step_hooks(testdir):
def when_dependency_fails(dependency): def when_dependency_fails(dependency):
pass pass
@scenario('test.feature', "When step's dependency a has failure") @mark.scenario('test.feature', "When step's dependency a has failure")
def test_when_dependency_fails(): def test_when_dependency_fails():
pass pass
@scenario('test.feature', 'When step has hook on failure') @mark.scenario('test.feature', 'When step has hook on failure')
def test_when_fails(): def test_when_fails():
pass pass
@scenario('test.feature', 'When step is not found') @mark.scenario('test.feature', 'When step is not found')
def test_when_not_found(): def test_when_not_found():
pass pass
@ -134,7 +134,7 @@ def test_step_hooks(testdir):
def foo(): def foo():
return 'foo' return 'foo'
@scenario('test.feature', 'When step validation error happens') @mark.scenario('test.feature', 'When step validation error happens')
def test_when_step_validation_error(): def test_when_step_validation_error():
pass pass
""") """)
@ -185,7 +185,7 @@ def test_step_trace(testdir):
""") """)
testdir.makepyfile(""" testdir.makepyfile("""
import pytest import pytest
from pytest_bdd import given, when, scenario from pytest_bdd import given, when, scenario, mark
@given('I have a bar') @given('I have a bar')
def i_have_bar(): def i_have_bar():
@ -197,7 +197,7 @@ def test_step_trace(testdir):
test_when_fails_inline = scenario('test.feature', 'When step has failure') test_when_fails_inline = scenario('test.feature', 'When step has failure')
@scenario('test.feature', 'When step has failure') @mark.scenario('test.feature', 'When step has failure')
def test_when_fails_decorated(): def test_when_fails_decorated():
pass pass

View File

@ -33,9 +33,8 @@ def then_nevermind():
def test_wrong(request, feature, scenario_name): def test_wrong(request, feature, scenario_name):
"""Test wrong feature scenarios.""" """Test wrong feature scenarios."""
sc = scenario(feature, scenario_name)
with pytest.raises(FeatureError): with pytest.raises(FeatureError):
sc(request) scenario(feature, scenario_name)
# TODO: assert the exception args from parameters # TODO: assert the exception args from parameters
@ -60,9 +59,8 @@ def test_wrong_type_order(request, scenario_name):
def test_verbose_output(request): def test_verbose_output(request):
"""Test verbose output of failed feature scenario""" """Test verbose output of failed feature scenario"""
sc = scenario('when_after_then.feature', 'When after then')
with pytest.raises(FeatureError) as excinfo: with pytest.raises(FeatureError) as excinfo:
sc(request) scenario('when_after_then.feature', 'When after then')
msg, line_number, line = excinfo.value.args msg, line_number, line = excinfo.value.args