feature outline

This commit is contained in:
Anatoly Bubenkov 2015-06-20 23:46:32 +02:00
parent 2b96f61268
commit 8af2e0f0e0
5 changed files with 124 additions and 59 deletions

View File

@ -21,9 +21,19 @@ class ScenarioNotFound(ScenarioValidationError):
"""Scenario Not Found.""" """Scenario Not Found."""
class ExamplesNotValidError(ScenarioValidationError):
"""Example table is not valid."""
class ScenarioExamplesNotValidError(ScenarioValidationError): class ScenarioExamplesNotValidError(ScenarioValidationError):
"""Scenario steps argumets do not match declared scenario examples.""" """Scenario steps parameters do not match declared scenario examples."""
class FeatureExamplesNotValidError(ScenarioValidationError):
"""Feature example table is not valid."""
class StepTypeError(ScenarioValidationError): class StepTypeError(ScenarioValidationError):

View File

@ -174,6 +174,78 @@ def get_features(paths, **kwargs):
return features return features
class Examples(object):
"""Example table."""
def __init__(self):
"""Initialize examples instance."""
self.example_params = []
self.examples = []
self.vertical_examples = []
def __nonzero__(self):
"""Bool check equals to the example list check."""
return self.__bool__()
def __bool__(self):
"""Bool check equals to the example list check."""
return bool(self.examples or self.vertical_examples)
def set_param_names(self, keys):
"""Set parameter names.
:param names: `list` of `string` parameter names.
"""
self.example_params = [str(key) for key in keys]
def add_example(self, values):
"""Add example.
:param values: `list` of `string` parameter values.
"""
self.examples.append(values)
def add_example_row(self, param, values):
"""Add example row.
:param param: `str` parameter name
:param values: `list` of `string` parameter values
"""
if param in self.example_params:
raise exceptions.ExamplesNotValidError(
"""Example rows should contain unique parameters. {0} appeared more than once.""".format(
param,
)
)
self.example_params.append(param)
self.vertical_examples.append(values)
def get_params(self, converters):
"""Get scenario pytest parametrization table.
:param converters: `dict` of converter functions to convert parameter values
"""
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 converters and param in converters:
example[index] = converters[param](example[index])
params.append(example)
return [self.example_params, params]
else:
return []
class Feature(object): class Feature(object):
"""Feature.""" """Feature."""
@ -193,6 +265,7 @@ class Feature(object):
self.line_number = 1 self.line_number = 1
self.name = None self.name = None
self.tags = set() self.tags = set()
self.examples = Examples()
scenario = None scenario = None
mode = None mode = None
prev_mode = None prev_mode = None
@ -266,13 +339,20 @@ class Feature(object):
elif mode == types.EXAMPLES_VERTICAL: elif mode == types.EXAMPLES_VERTICAL:
mode = types.EXAMPLE_LINE_VERTICAL mode = types.EXAMPLE_LINE_VERTICAL
elif mode == types.EXAMPLES_HEADERS: elif mode == types.EXAMPLES_HEADERS:
scenario.set_param_names([l.strip() for l in parsed_line.split("|")[1:-1] if l.strip()]) (scenario or self).examples.set_param_names(
[l.strip() for l in parsed_line.split("|")[1:-1] if l.strip()])
mode = types.EXAMPLE_LINE mode = types.EXAMPLE_LINE
elif mode == types.EXAMPLE_LINE: elif mode == types.EXAMPLE_LINE:
scenario.add_example([l.strip() for l in stripped_line.split("|")[1:-1]]) (scenario or self).examples.add_example([l.strip() for l in stripped_line.split("|")[1:-1]])
elif mode == types.EXAMPLE_LINE_VERTICAL: elif mode == types.EXAMPLE_LINE_VERTICAL:
param_line_parts = [l.strip() for l in stripped_line.split("|")[1:-1]] param_line_parts = [l.strip() for l in stripped_line.split("|")[1:-1]]
scenario.add_example_row(param_line_parts[0], param_line_parts[1:]) try:
(scenario or self).examples.add_example_row(param_line_parts[0], param_line_parts[1:])
except exceptions.ExamplesNotValidError as exc:
if scenario:
raise exceptions.ScenarioExamplesNotValidError(exc.args[0])
else:
raise exceptions.FeatureExamplesNotValidError(exc.args[0])
elif mode and mode not in (types.FEATURE, types.TAG): elif mode and mode not in (types.FEATURE, types.TAG):
step = Step( step = Step(
name=parsed_line, name=parsed_line,
@ -330,9 +410,7 @@ class Scenario(object):
self.feature = feature self.feature = feature
self.name = name self.name = name
self._steps = [] self._steps = []
self.example_params = [] self.examples = Examples()
self.examples = []
self.vertical_examples = []
self.line_number = line_number self.line_number = line_number
self.example_converters = example_converters self.example_converters = example_converters
self.tags = tags or set() self.tags = tags or set()
@ -368,56 +446,13 @@ class Scenario(object):
""" """
return frozenset(sum((list(step.params) for step in self.steps), [])) return frozenset(sum((list(step.params) for step in self.steps), []))
def set_param_names(self, keys): def get_examples(self):
"""Set parameter names. """Get examples."""
return self.examples or self.feature.examples
:param names: `list` of `string` parameter names.
"""
self.example_params = [str(key) for key in keys]
def add_example(self, values):
"""Add example.
:param values: `list` of `string` parameter values.
"""
self.examples.append(values)
def add_example_row(self, param, values):
"""Add example row.
:param param: `str` parameter name
:param values: `list` of `string` parameter values
"""
if param in self.example_params:
raise exceptions.ScenarioExamplesNotValidError(
"""Scenario "{0}" in the feature "{1}" has not valid examples. """
"""Example rows should contain unique parameters. {2} appeared more than once.""".format(
self.name, self.feature.filename, param,
)
)
self.example_params.append(param)
self.vertical_examples.append(values)
def get_params(self): def get_params(self):
"""Get scenario pytest parametrization table.""" """Get example params."""
param_count = len(self.example_params) return self.get_examples().get_params(self.example_converters)
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): def validate(self):
"""Validate the scenario. """Validate the scenario.
@ -425,11 +460,11 @@ class Scenario(object):
:raises ScenarioValidationError: when scenario is not valid :raises ScenarioValidationError: when scenario is not valid
""" """
params = self.params params = self.params
if params and self.example_params and params != set(self.example_params): if params and self.examples.example_params and params != set(self.examples.example_params):
raise exceptions.ScenarioExamplesNotValidError( raise exceptions.ScenarioExamplesNotValidError(
"""Scenario "{0}" in the feature "{1}" has not valid examples. """ """Scenario "{0}" in the feature "{1}" has not valid examples. """
"""Set of step parameters {2} should match set of example values {3}.""".format( """Set of step parameters {2} should match set of example values {3}.""".format(
self.name, self.feature.filename, sorted(params), sorted(self.example_params), self.name, self.feature.filename, sorted(params), sorted(self.examples.example_params),
) )
) )

View File

@ -250,7 +250,7 @@ def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, call
args = inspect.getargspec(_pytestbdd_function).args args = inspect.getargspec(_pytestbdd_function).args
function_args = list(args) function_args = list(args)
for arg in scenario.example_params: for arg in scenario.get_examples().example_params:
if arg not in function_args: if arg not in function_args:
function_args.append(arg) function_args.append(arg)
if "request" not in function_args: if "request" not in function_args:
@ -272,7 +272,6 @@ def _get_scenario_decorator(feature, feature_name, scenario, scenario_name, call
) )
params = scenario.get_params() params = scenario.get_params()
if params: if params:
_scenario = pytest.mark.parametrize(*params)(_scenario) _scenario = pytest.mark.parametrize(*params)(_scenario)

View File

@ -0,0 +1,11 @@
Feature: Outline
Examples:
| start | eat | left |
| 12 | 5 | 7 |
| 5 | 4 | 1 |
Scenario Outline: Outlined given, when, thens
Given there are <start> cucumbers
When I eat <eat> cucumbers
Then I should have <left> cucumbers

View File

@ -103,3 +103,13 @@ def test_empty_example_values():
assert test_scenario.parametrize.args == ( assert test_scenario.parametrize.args == (
[u'start', u'eat', u'left'], [['#', '', '']]) [u'start', u'eat', u'left'], [['#', '', '']])
@scenario(
'outline_feature.feature',
'Outlined given, when, thens',
example_converters=dict(start=int, eat=float, left=str)
)
def test_outlined_feature():
assert test_outlined.parametrize.args == (
[u'start', u'eat', u'left'], [[12, 5.0, '7'], [5, 4.0, '1']])