multiline steps implemented

This commit is contained in:
Anatoly Bubenkov 2014-03-29 00:58:38 +01:00
parent e935d6eee9
commit 60591ced61
6 changed files with 130 additions and 27 deletions

View File

@ -1,6 +1,12 @@
Changelog
=========
2.1.0
-----
- Implemented multiline steps (bubenkoff)
2.0.1
-----

View File

@ -38,7 +38,7 @@ Example
publish_article.feature:
.. code-block:: gherkin
Feature: Blog
A site where you can publish your articles.
@ -174,6 +174,61 @@ Example code also shows possibility to pass argument converters which may be use
different than strings.
Multiline steps
---------------
As Gherkin, pytest-bdd supports multiline steps (aka `PyStrings <http://docs.behat.org/guides/1.gherkin.html#pystrings>`_).
But in much cleaner and powerful way:
.. code-block:: gherkin
Scenario: Multiline step using sub indentation
Given I have a step with:
Some
Extra
Lines
Then the text should be parsed with correct indentation
Step is considered as multiline one, if the **next** line(s) after it's first line, is indented relatively
to the first line. The step name is then simply extended by adding futher lines with newlines.
In the example above, the Given step name will be:
.. code-block:: python
"""I have a step with:\nSome\nExtra\nLines"""
You can of course register step using full name (including the newlines), but it seems more practical to use
step arguments and capture lines after first line (or some subset of them) into the argument:
.. code-block:: python
import re
from pytest_bdd import given, then, scenario
@scenario(
'multiline.feature',
'Multiline step using sub indentation',
)
def test_multiline():
pass
@given(re.compile(r'I have a step with:\n(?P<text>.+)', re.DOTALL))
def i_have_text(text):
return text
@then('the text should be parsed with correct indentation')
def eat_cucumbers(i_have_text, text):
assert i_have_text == text == """Some
Extra
Lines"""
Pay attention to the re.DOTALL option used for step registration. When used, .+ will also capture newlines.
Scenario parameters
-------------------
Scenario decorator can accept such optional keyword arguments:

View File

@ -146,52 +146,62 @@ class Feature(object):
with _open_file(filename, encoding) as f:
content = force_unicode(f.read(), encoding)
step = None
for line_number, line in enumerate(content.splitlines()):
raw_line = line.strip()
line = strip_comments(line)
if not line:
unindented_line = line.lstrip()
line_indent = len(line) - len(unindented_line)
if step and step.indent < line_indent:
# multiline step, so just add line and continue
step.add_line(line.lstrip())
continue
mode = get_step_type(line) or mode
stripped_line = line.strip()
clean_line = strip_comments(line)
if not clean_line:
continue
mode = get_step_type(clean_line) or mode
if mode == types.GIVEN and prev_mode not in (types.GIVEN, types.SCENARIO, types.SCENARIO_OUTLINE):
raise FeatureError('Given steps must be the first in withing the Scenario',
line_number, line)
line_number, clean_line)
if mode == types.WHEN and prev_mode not in (
types.SCENARIO, types.SCENARIO_OUTLINE, types.GIVEN, types.WHEN):
raise FeatureError('When steps must be the first or follow Given steps',
line_number, line)
line_number, clean_line)
if mode == types.THEN and prev_mode not in (types.GIVEN, types.WHEN, types.THEN):
raise FeatureError('Then steps must follow Given or When steps',
line_number, line)
line_number, clean_line)
if mode == types.FEATURE:
if prev_mode != types.FEATURE:
self.name = remove_prefix(line)
self.name = remove_prefix(clean_line)
else:
description.append(line)
description.append(clean_line)
prev_mode = mode
# Remove Feature, Given, When, Then, And
line = remove_prefix(line)
clean_line = remove_prefix(clean_line)
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
self.scenarios[line] = scenario = Scenario(self, line)
self.scenarios[clean_line] = scenario = Scenario(self, clean_line)
step = None
elif mode == types.EXAMPLES:
step = None
mode = types.EXAMPLES_HEADERS
elif mode == types.EXAMPLES_VERTICAL:
mode = types.EXAMPLE_LINE_VERTICAL
elif mode == types.EXAMPLES_HEADERS:
scenario.set_param_names([l.strip() for l in line.split('|')[1:-1] if l.strip()])
scenario.set_param_names([l.strip() for l in clean_line.split('|')[1:-1] if l.strip()])
mode = types.EXAMPLE_LINE
elif mode == types.EXAMPLE_LINE:
scenario.add_example([l.strip() for l in raw_line.split('|')[1:-1]])
scenario.add_example([l.strip() for l in stripped_line.split('|')[1:-1]])
elif mode == types.EXAMPLE_LINE_VERTICAL:
line = [l.strip() for l in raw_line.split('|')[1:-1]]
scenario.add_example_row(line[0], line[1:])
step = None
clean_line = [l.strip() for l in stripped_line.split('|')[1:-1]]
scenario.add_example_row(clean_line[0], clean_line[1:])
elif mode and mode != types.FEATURE:
scenario.add_step(step_name=line, step_type=mode)
step = scenario.add_step(step_name=clean_line, step_type=mode, indent=line_indent)
self.description = u'\n'.join(description)
@ -228,7 +238,7 @@ class Scenario(object):
self.vertical_examples = []
self.example_converters = example_converters
def add_step(self, step_name, step_type):
def add_step(self, step_name, step_type, indent):
"""Add step to the scenario.
:param step_name: Step name.
@ -237,7 +247,9 @@ class Scenario(object):
"""
params = get_step_params(step_name)
self.params.update(params)
self.steps.append(Step(name=step_name, type=step_type, params=params))
step = Step(name=step_name, type=step_type, params=params, scenario=self, indent=indent)
self.steps.append(step)
return step
def set_param_names(self, keys):
"""Set parameter names.
@ -311,7 +323,13 @@ class Scenario(object):
class Step(object):
"""Step."""
def __init__(self, name, type, params):
def __init__(self, name, type, params, scenario, indent):
self.name = name
self.indent = indent
self.type = type
self.params = params
self.scenario = scenario
def add_line(self, line):
"""Add line to the multiple step."""
self.name += '\n' + line

View File

@ -6,7 +6,7 @@ from setuptools import setup
from setuptools.command.test import test as TestCommand
version = '2.0.1'
version = '2.1.0'
class Tox(TestCommand):

View File

@ -1,6 +1,6 @@
Feature: Multiline step using sub indentation
Given I have a step with:
Some
Extra
Lines
Then text should be parsed with correct indentation
Scenario: Multiline step using sub indentation
Given I have a step with:
Some
Extra
Lines
Then the text should be parsed with correct indentation

View File

@ -0,0 +1,24 @@
"""Multiline steps tests."""
import re
from pytest_bdd import given, then, scenario
@scenario(
'multiline.feature',
'Multiline step using sub indentation',
)
def test_multiline():
pass
@given(re.compile(r'I have a step with:\n(?P<text>.+)', re.DOTALL))
def i_have_text(text):
return text
@then('the text should be parsed with correct indentation')
def eat_cucumbers(i_have_text, text):
assert i_have_text == text == """Some
Extra
Lines"""