Scenario outline implementation based on pure pytest parametrization
This commit is contained in:
parent
f20c02a571
commit
bca5206677
|
@ -1,6 +1,13 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
2.0.0
|
||||
-----
|
||||
|
||||
- Pure pytest parametrization for scenario outlines (bubenkoff)
|
||||
- Splitting scenario decorated and non-decorated variants (bubenkoff)
|
||||
|
||||
|
||||
1.0.0
|
||||
-----
|
||||
|
||||
|
|
65
README.rst
65
README.rst
|
@ -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
|
||||
containing the side effects of the Gherkin imperative declarations.
|
||||
|
||||
|
||||
Install pytest-bdd
|
||||
==================
|
||||
|
||||
|
@ -28,6 +29,7 @@ Install pytest-bdd
|
|||
|
||||
pip install pytest-bdd
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
|
@ -81,6 +83,7 @@ test\_publish\_article.py:
|
|||
article.refresh() # Refresh the object in the SQLAlchemy session
|
||||
assert article.is_published
|
||||
|
||||
|
||||
Step aliases
|
||||
============
|
||||
|
||||
|
@ -113,6 +116,7 @@ default author.
|
|||
Given I'm the admin
|
||||
And there is an article
|
||||
|
||||
|
||||
Step arguments
|
||||
==============
|
||||
|
||||
|
@ -143,28 +147,32 @@ The code will look like:
|
|||
|
||||
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):
|
||||
# note that you always get step arguments as strings, convert them on demand
|
||||
start = int(start)
|
||||
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):
|
||||
eat = int(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):
|
||||
start, left = int(start), int(left)
|
||||
assert start_cucumbers['start'] == start
|
||||
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 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
|
||||
=================
|
||||
|
@ -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
|
||||
exactly as it's described in be behave docs.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: feature
|
||||
|
@ -199,42 +206,47 @@ The code will look like:
|
|||
test_outlined = scenario(
|
||||
'outline.feature',
|
||||
'Outlined given, when, thens',
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
def start_cucumbers(start):
|
||||
return dict(start=int(start))
|
||||
assert isinstance(start, int)
|
||||
return dict(start=start)
|
||||
|
||||
|
||||
@when('I eat <eat> cucumbers')
|
||||
def eat_cucumbers(start_cucumbers, start, eat):
|
||||
start_cucumbers['eat'] = int(eat)
|
||||
def eat_cucumbers(start_cucumbers, eat):
|
||||
assert isinstance(eat, float)
|
||||
start_cucumbers['eat'] = 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)
|
||||
assert isinstance(left, str)
|
||||
assert start - eat == int(left)
|
||||
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.
|
||||
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.
|
||||
Example code also shows possibility to pass example converters which may be useful if you need parameter types
|
||||
different than strings.
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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
|
||||
@pytest.mark.parametrize(
|
||||
['start', 'eat', 'left'],
|
||||
[(12, 5, 7)])
|
||||
@scenario(
|
||||
@mark.scenario(
|
||||
'parametrized.feature',
|
||||
'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.
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
Subplugins
|
||||
==========
|
||||
Browser testing
|
||||
===============
|
||||
|
||||
The pytest BDD has plugin support, and the main purpose of plugins
|
||||
(subplugins) is to provide useful and specialized fixtures.
|
||||
Tools recommended to use for browser testing:
|
||||
|
||||
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
|
||||
=======
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from pytest_bdd.steps import given, when, then # 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__] # pragma: no cover
|
||||
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, mark.__name__] # pragma: no cover
|
||||
|
|
|
@ -229,8 +229,9 @@ class Scenario(object):
|
|||
:param step_type: Step type.
|
||||
|
||||
"""
|
||||
self.params.update(get_step_params(step_name))
|
||||
self.steps.append(Step(name=step_name, type=step_type))
|
||||
params = get_step_params(step_name)
|
||||
self.params.update(params)
|
||||
self.steps.append(Step(name=step_name, type=step_type, params=params))
|
||||
|
||||
def set_param_names(self, keys):
|
||||
"""Set parameter names.
|
||||
|
@ -238,8 +239,7 @@ class Scenario(object):
|
|||
:param names: `list` of `string` parameter names
|
||||
|
||||
"""
|
||||
self.params.update(keys)
|
||||
self.example_params = keys
|
||||
self.example_params = [str(key) for key in keys]
|
||||
|
||||
def add_example(self, values):
|
||||
"""Add example.
|
||||
|
@ -253,6 +253,7 @@ class Scenario(object):
|
|||
class Step(object):
|
||||
"""Step."""
|
||||
|
||||
def __init__(self, name, type):
|
||||
def __init__(self, name, type, params):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.params = params
|
||||
|
|
|
@ -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)
|
|
@ -11,15 +11,26 @@ test_publish_article = scenario(
|
|||
)
|
||||
|
||||
"""
|
||||
import collections
|
||||
import os
|
||||
import imp
|
||||
|
||||
import sys
|
||||
import inspect # 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_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.types import GIVEN
|
||||
|
||||
from pytest_bdd import plugin
|
||||
|
||||
|
||||
class ScenarioValidationError(Exception):
|
||||
"""Base class for scenario validation."""
|
||||
|
@ -29,8 +40,8 @@ class ScenarioNotFound(ScenarioValidationError): # pragma: no cover
|
|||
"""Scenario Not Found"""
|
||||
|
||||
|
||||
class NotEnoughScenarioParams(ScenarioValidationError): # pragma: no cover
|
||||
"""Scenario function doesn't take enough parameters in the arguments."""
|
||||
class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover
|
||||
"""Scenario steps argumets do not match declared scenario examples."""
|
||||
|
||||
|
||||
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
|
||||
|
||||
if match:
|
||||
converters = getattr(fixturedef.func, 'converters', {})
|
||||
for arg, value in match.groupdict().items():
|
||||
if arg in converters:
|
||||
value = converters[arg](value)
|
||||
_inject_fixture(request, arg, value)
|
||||
return request.getfuncargvalue(pattern.pattern)
|
||||
raise
|
||||
|
@ -109,26 +123,58 @@ def _find_step_function(request, name, encoding):
|
|||
|
||||
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),
|
||||
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_scenario_outline(feature, scenario, request, encoding):
|
||||
def _execute_scenario_outline(feature, scenario, request, encoding, example_converters=None):
|
||||
"""Execute the scenario outline."""
|
||||
errors = []
|
||||
# tricky part, basically here we clear pytest request cache
|
||||
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)
|
||||
_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."""
|
||||
|
||||
_validate_scenario(feature, scenario, request)
|
||||
|
@ -165,68 +211,77 @@ def _execute_scenario(feature, scenario, request, encoding):
|
|||
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
|
||||
_execute_step_function(request, feature, step, step_func, example=example)
|
||||
|
||||
|
||||
def scenario(feature_name, scenario_name, encoding='utf-8'):
|
||||
"""Scenario. May be called both as decorator and as just normal function."""
|
||||
FakeRequest = collections.namedtuple('FakeRequest', ['module'])
|
||||
|
||||
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):
|
||||
# Get the feature
|
||||
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)
|
||||
if not module:
|
||||
module = caller_module
|
||||
|
||||
# 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)
|
||||
)
|
||||
if hasattr(module, fixture):
|
||||
return call_fixture(getattr(module, fixture))
|
||||
|
||||
if scenario.examples:
|
||||
_execute_scenario_outline(feature, scenario, request, encoding)
|
||||
else:
|
||||
_execute_scenario(feature, scenario, request, encoding)
|
||||
if path is None:
|
||||
path = os.path.dirname(module.__file__)
|
||||
if os.path.exists(os.path.join(path, '__init__.py')):
|
||||
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):
|
||||
# Called as a normal function.
|
||||
_scenario = recreate_function(_scenario, module=caller_module)
|
||||
return _scenario(request)
|
||||
def scenario(
|
||||
feature_name, scenario_name, encoding='utf-8', example_converters=None,
|
||||
caller_module=None, caller_function=None):
|
||||
"""Scenario."""
|
||||
|
||||
# Used as a decorator. Modify the returned function to add parameters from a decorated function.
|
||||
func_args = inspect.getargspec(request).args
|
||||
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)
|
||||
caller_module = caller_module or get_caller_module()
|
||||
caller_function = caller_function or get_caller_function()
|
||||
|
||||
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
|
||||
|
|
|
@ -51,11 +51,13 @@ class StepError(Exception): # 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.
|
||||
|
||||
:param name: Given step name.
|
||||
: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.
|
||||
: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()
|
||||
step_func = lambda request: request.getfuncargvalue(fixture)
|
||||
step_func.step_type = GIVEN
|
||||
step_func.converters = converters
|
||||
step_func.__name__ = name
|
||||
step_func.fixture = fixture
|
||||
func = pytest.fixture(lambda: step_func)
|
||||
|
@ -73,29 +76,33 @@ def given(name, fixture=None):
|
|||
contribute_to_module(module, remove_prefix(name), func)
|
||||
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.
|
||||
|
||||
: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.
|
||||
|
||||
"""
|
||||
return _step_decorator(WHEN, name)
|
||||
return _step_decorator(WHEN, name, converters=converters)
|
||||
|
||||
|
||||
def then(name):
|
||||
def then(name, converters=None):
|
||||
"""Then step decorator.
|
||||
|
||||
: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.
|
||||
|
||||
"""
|
||||
return _step_decorator(THEN, name)
|
||||
return _step_decorator(THEN, name, converters=converters)
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
def _step_decorator(step_type, step_name):
|
||||
def _step_decorator(step_type, step_name, converters=None):
|
||||
"""Step decorator for the type and the name.
|
||||
|
||||
: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.step_type = step_type
|
||||
step_func.converters = converters
|
||||
|
||||
@pytest.fixture
|
||||
def lazy_step_func():
|
||||
|
@ -151,6 +159,8 @@ def _step_decorator(step_type, step_name):
|
|||
|
||||
if pattern:
|
||||
lazy_step_func.pattern = pattern
|
||||
if converters:
|
||||
lazy_step_func.converters = converters
|
||||
|
||||
contribute_to_module(
|
||||
get_caller_module(),
|
||||
|
@ -162,7 +172,7 @@ def _step_decorator(step_type, step_name):
|
|||
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.
|
||||
|
||||
:param func: Function object.
|
||||
|
@ -188,6 +198,10 @@ def recreate_function(func, module=None, name=None, add_args=(), firstlineno=Non
|
|||
if PY3:
|
||||
argnames.insert(1, 'co_kwonlyargcount')
|
||||
|
||||
for arg in inspect.getargspec(func).args:
|
||||
if arg in add_args:
|
||||
add_args.remove(arg)
|
||||
|
||||
args = []
|
||||
code = get_code(func)
|
||||
for arg in argnames:
|
||||
|
|
3
setup.py
3
setup.py
|
@ -6,7 +6,7 @@ from setuptools import setup
|
|||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
version = '1.0.0'
|
||||
version = '2.0.0'
|
||||
|
||||
|
||||
class Tox(TestCommand):
|
||||
|
@ -54,6 +54,7 @@ setup(
|
|||
cmdclass={'test': Tox},
|
||||
install_requires=[
|
||||
'pytest',
|
||||
'future'
|
||||
],
|
||||
# the following makes a plugin available to py.test
|
||||
entry_points={
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
Scenario: Every step takes a parameter with the same name
|
||||
Given I have 1 Euro
|
||||
When I pay 2 Euro
|
||||
And I pay 1 Euro
|
||||
Then I should have 0 Euro
|
||||
And I should have 999999 Euro # In my dream...
|
||||
Given I have 1 Euro
|
||||
When I pay 2 Euro
|
||||
And I pay 1 Euro
|
||||
Then I should have 0 Euro
|
||||
And I should have 999999 Euro # In my dream...
|
||||
|
||||
|
||||
Scenario: Using the same given fixture raises an error
|
||||
Given I have 1 Euro
|
||||
And I have 2 Euro
|
||||
Given I have 1 Euro
|
||||
And I have 2 Euro
|
||||
|
|
|
@ -19,19 +19,24 @@ test_argument_in_when_step_2 = sc('Argument in when, step 2')
|
|||
|
||||
@pytest.fixture
|
||||
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):
|
||||
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):
|
||||
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')
|
||||
def argument():
|
||||
"""I have an argument."""
|
||||
|
@ -44,11 +49,6 @@ def get_argument(argument, 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+)'))
|
||||
def assert_that_my_argument_is_arg(argument, arg):
|
||||
"""Assert that arg from when equals arg."""
|
||||
|
|
|
@ -6,3 +6,24 @@ Scenario Outline: Outlined given, when, thens
|
|||
Examples:
|
||||
| start | eat | left |
|
||||
| 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 |
|
||||
|
|
|
@ -20,8 +20,6 @@ def pytestbdd_feature_base_dir():
|
|||
|
||||
def test_feature_path(request, scenario_name):
|
||||
"""Test feature base dir."""
|
||||
sc = scenario('steps.feature', scenario_name)
|
||||
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)
|
||||
|
|
|
@ -1,25 +1,55 @@
|
|||
"""Scenario Outline tests."""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
from pytest_bdd import mark
|
||||
from pytest_bdd.scenario import ScenarioExamplesNotValidError
|
||||
|
||||
|
||||
test_outlined = scenario(
|
||||
'outline.feature',
|
||||
'Outlined given, when, thens',
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
def start_cucumbers(start):
|
||||
return dict(start=int(start))
|
||||
assert isinstance(start, int)
|
||||
return dict(start=start)
|
||||
|
||||
|
||||
@when('I eat <eat> cucumbers')
|
||||
def eat_cucumbers(start_cucumbers, start, eat):
|
||||
start_cucumbers['eat'] = int(eat)
|
||||
def eat_cucumbers(start_cucumbers, eat):
|
||||
assert isinstance(eat, float)
|
||||
start_cucumbers['eat'] = 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)
|
||||
assert isinstance(left, str)
|
||||
assert start - eat == int(left)
|
||||
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]
|
||||
)
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import pytest
|
||||
|
||||
from pytest_bdd.scenario import NotEnoughScenarioParams
|
||||
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
from pytest_bdd import given, when, then, mark
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
['start', 'eat', 'left'],
|
||||
[(12, 5, 7)])
|
||||
@scenario(
|
||||
@mark.scenario(
|
||||
'parametrized.feature',
|
||||
'Parametrized given, when, thens',
|
||||
)
|
||||
|
@ -18,37 +16,18 @@ def test_parametrized(request, start, eat, left):
|
|||
|
||||
@pytest.fixture(params=[1, 2])
|
||||
def foo_bar(request):
|
||||
return 'foo_bar' * request.param
|
||||
return 'bar' * request.param
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
['start', 'eat', 'left'],
|
||||
[(12, 5, 7)])
|
||||
@scenario(
|
||||
@mark.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(
|
||||
'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: []."""
|
||||
)
|
||||
"""Test parametrized scenario, but also with other parametrized fixtures."""
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
|
|
|
@ -6,10 +6,9 @@ from pytest_bdd.scenario import ScenarioNotFound
|
|||
|
||||
def test_scenario_not_found(request):
|
||||
"""Test the situation when scenario is not found."""
|
||||
test_not_found = scenario(
|
||||
'not_found.feature',
|
||||
'NOT FOUND'
|
||||
)
|
||||
|
||||
with pytest.raises(ScenarioNotFound):
|
||||
test_not_found(request)
|
||||
scenario(
|
||||
'not_found.feature',
|
||||
'NOT FOUND'
|
||||
)
|
||||
|
|
|
@ -96,7 +96,7 @@ def test_step_hooks(testdir):
|
|||
""")
|
||||
testdir.makepyfile("""
|
||||
import pytest
|
||||
from pytest_bdd import given, when, scenario
|
||||
from pytest_bdd import given, when, mark
|
||||
|
||||
@given('I have a bar')
|
||||
def i_have_bar():
|
||||
|
@ -118,15 +118,15 @@ def test_step_hooks(testdir):
|
|||
def when_dependency_fails(dependency):
|
||||
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():
|
||||
pass
|
||||
|
||||
@scenario('test.feature', 'When step has hook on failure')
|
||||
@mark.scenario('test.feature', 'When step has hook on failure')
|
||||
def test_when_fails():
|
||||
pass
|
||||
|
||||
@scenario('test.feature', 'When step is not found')
|
||||
@mark.scenario('test.feature', 'When step is not found')
|
||||
def test_when_not_found():
|
||||
pass
|
||||
|
||||
|
@ -134,7 +134,7 @@ def test_step_hooks(testdir):
|
|||
def 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():
|
||||
pass
|
||||
""")
|
||||
|
@ -185,7 +185,7 @@ def test_step_trace(testdir):
|
|||
""")
|
||||
testdir.makepyfile("""
|
||||
import pytest
|
||||
from pytest_bdd import given, when, scenario
|
||||
from pytest_bdd import given, when, scenario, mark
|
||||
|
||||
@given('I have a 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')
|
||||
|
||||
@scenario('test.feature', 'When step has failure')
|
||||
@mark.scenario('test.feature', 'When step has failure')
|
||||
def test_when_fails_decorated():
|
||||
pass
|
||||
|
||||
|
|
|
@ -33,9 +33,8 @@ def then_nevermind():
|
|||
def test_wrong(request, feature, scenario_name):
|
||||
"""Test wrong feature scenarios."""
|
||||
|
||||
sc = scenario(feature, scenario_name)
|
||||
with pytest.raises(FeatureError):
|
||||
sc(request)
|
||||
scenario(feature, scenario_name)
|
||||
# TODO: assert the exception args from parameters
|
||||
|
||||
|
||||
|
@ -60,9 +59,8 @@ def test_wrong_type_order(request, scenario_name):
|
|||
|
||||
def test_verbose_output(request):
|
||||
"""Test verbose output of failed feature scenario"""
|
||||
sc = scenario('when_after_then.feature', 'When after then')
|
||||
with pytest.raises(FeatureError) as excinfo:
|
||||
sc(request)
|
||||
scenario('when_after_then.feature', 'When after then')
|
||||
|
||||
msg, line_number, line = excinfo.value.args
|
||||
|
||||
|
|
Loading…
Reference in New Issue