Merge pull request #96 from bubenkoff/pluggable-parsers

Pluggable parsers for step definitions.
This commit is contained in:
Oleg Pidsadnyi 2015-01-07 16:27:51 +01:00
commit 8289cdc17a
23 changed files with 553 additions and 114 deletions

View File

@ -5,6 +5,7 @@ Unreleased
----------
- Correctly handle `pytest-bdd` command called without the subcommand under python3 (bubenkoff, spinus)
- Pluggable parsers for step definitions (bubenkoff, spinus)
2.5.3

View File

@ -23,6 +23,8 @@ 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.
.. _behave: https://pypi.python.org/pypi/behave
.. _pytest-splinter: https://github.com/paylogic/pytest-splinter
Install pytest-bdd
------------------
@ -36,7 +38,7 @@ Example
-------
An example test for a blog hosting software could look like this.
Note that `pytest-splinter <https://github.com/paylogic/pytest-splinter>`_ is used to get the browser fixture.
Note that pytest-splinter_ is used to get the browser fixture.
publish_article.feature:
@ -130,7 +132,62 @@ Step arguments
Often it's possible to reuse steps giving them a parameter(s).
This allows to have single implementation and multiple use, so less code.
Also opens the possibility to use same step twice in single scenario and with different arguments!
Important thing that argumented step names are not just strings but regular expressions.
And even more, there are several types of step parameter parsers at your disposal
(idea taken from behave_ implementation):
.. _pypi_parse: http://pypi.python.org/pypi/parse
.. _pypi_parse_type: http://pypi.python.org/pypi/parse_type
**string** (the default)
This is the default and can be considered as a `null` or `exact` parser. It parses no parameters
and matches the step name by equality of strings.
**parse** (based on: pypi_parse_)
Provides a simple parser that replaces regular expressions for
step parameters with a readable syntax like ``{param:Type}``.
The syntax is inspired by the Python builtin ``string.format()``
function.
Step parameters must use the named fields syntax of pypi_parse_
in step definitions. The named fields are extracted,
optionally type converted and then used as step function arguments.
Supports type conversions by using type converters passed via `extra_types`
**cfparse** (extends: pypi_parse_, based on: pypi_parse_type_)
Provides an extended parser with "Cardinality Field" (CF) support.
Automatically creates missing type converters for related cardinality
as long as a type converter for cardinality=1 is provided.
Supports parse expressions like:
* ``{values:Type+}`` (cardinality=1..N, many)
* ``{values:Type*}`` (cardinality=0..N, many0)
* ``{value:Type?}`` (cardinality=0..1, optional)
Supports type conversions (as above).
**re**
This uses full regular expressions to parse the clause text. You will
need to use named groups "(?P<name>...)" to define the variables pulled
from the text and passed to your ``step()`` function.
Type conversion can only be done via `converters` step decorator argument (see example below).
The default parser is `parse`, so you have to do nothing in addition to use it.
Parsers, as well as their optional arguments are specified like:
for `cfparse` parser
.. code-block:: python
from pytest_bdd import parsers
@given(parsers.cfparse('there are {start:Number} cucumbers', extra_types=dict(Number=int)))
def start_cucumbers(start):
return dict(start=start, eat=0)
for `re` parser
.. code-block:: python
from pytest_bdd import parsers
@given(parsers.re(r'there are (?P<start>\d+) cucumbers'), converters=dict(start=int))
def start_cucumbers(start):
return dict(start=start, eat=0)
Example:
@ -150,7 +207,7 @@ The code will look like:
.. code-block:: python
import re
from pytest_bdd import scenario, given, when, then
from pytest_bdd import scenario, given, when, then, parsers
@scenario('arguments.feature', 'Arguments for given, when, thens')
@ -158,29 +215,63 @@ The code will look like:
pass
@given(re.compile('there are (?P<start>\d+) cucumbers'), converters=dict(start=int))
@given(parsers.parse('there are {start:d} cucumbers'))
def start_cucumbers(start):
return dict(start=start, eat=0)
@when(re.compile('I eat (?P<eat>\d+) cucumbers'), converters=dict(eat=int))
@when(parsers.parse('I eat {eat:d} cucumbers'))
def eat_cucumbers(start_cucumbers, eat):
start_cucumbers['eat'] += eat
@then(re.compile('I should have (?P<left>\d+) cucumbers'), converters=dict(left=int))
@then(parsers.parse('I should have {left:d} cucumbers'))
def should_have_left_cucumbers(start_cucumbers, start, 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.
Example code also shows possibility to pass argument converters which may be useful if you need to postprocess step
arguments after the parser.
You can implement your own step parser. It's interface is quite simple. The code can looks like:
.. code-block:: python
import re
from pytest_bdd import given, parsers
class MyParser(parsers.StepParser):
"""Custom parser."""
def __init__(self, name, **kwargs):
"""Compile regex."""
super(re, self).__init__(name)
self.regex = re.compile(re.sub('%(.+)%', '(?P<\1>.+)', self.name), **kwargs)
def parse_arguments(self, name):
"""Get step arguments.
:return: `dict` of step arguments
"""
return self.regex.match(name).groupdict()
def is_matching(self, name):
"""Match given name with the step name."""
return bool(self.regex.match(name))
@given(parsers.parse('there are %start% cucumbers'))
def start_cucumbers(start):
return dict(start=start, eat=0)
Multiline steps
---------------
As Gherkin, pytest-bdd supports multiline steps (aka `PyStrings <http://docs.behat.org/guides/1.gherkin.html#pystrings>`_).
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
@ -245,8 +336,8 @@ 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.
`Gherkin 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:
@ -532,7 +623,7 @@ Backgrounds
It's often the case that to cover certain feature, you'll need multiple scenarios. And it's logical that the
setup for those scenarios will have some common parts (if not equal). For this, there are `backgrounds`.
pytest-bdd implements gherkin `backgrounds <http://docs.behat.org/en/v2.5/guides/1.gherkin.html#backgrounds>`_ for
pytest-bdd implements `Gherkin backgrounds <http://docs.behat.org/en/v2.5/guides/1.gherkin.html#backgrounds>`_ for
features.
.. code-block:: gherkin
@ -721,13 +812,14 @@ test_publish_article.py:
pass
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
-----
pytest-bdd exposes several pytest `hooks <http://pytest.org/latest/plugins.html#well-specified-hooks>`_
pytest-bdd exposes several `pytest hooks <http://pytest.org/latest/plugins.html#well-specified-hooks>`_
which might be helpful building useful reporting, visualization, etc on top of it:
* pytest_bdd_before_scenario(request, feature, scenario) - Called before scenario is executed
@ -755,7 +847,7 @@ Browser testing
Tools recommended to use for browser testing:
* `pytest-splinter <https://github.com/paylogic/pytest-splinter>`_ - pytest `splinter <http://splinter.cobrateam.info/>`_ integration for the real browser testing
* pytest-splinter_ - pytest `splinter <http://splinter.cobrateam.info/>`_ integration for the real browser testing
Reporting

View File

@ -6,11 +6,11 @@ import os
import time
import py
import six
from .feature import force_unicode
from .steps import PY3
if PY3:
if six.PY3:
long = int

View File

@ -1,29 +1,46 @@
"""pytest-bdd Exceptions."""
class StepError(Exception):
"""Step declaration error."""
class ScenarioIsDecoratorOnly(Exception):
"""Scenario can be only used as decorator."""
class ScenarioValidationError(Exception):
"""Base class for scenario validation."""
class ScenarioNotFound(ScenarioValidationError):
"""Scenario Not Found"""
"""Scenario Not Found."""
class ScenarioExamplesNotValidError(ScenarioValidationError):
"""Scenario steps argumets do not match declared scenario examples."""
class StepTypeError(ScenarioValidationError):
"""Step definition is not of the type expected in the scenario."""
class GivenAlreadyUsed(ScenarioValidationError):
"""Fixture that implements the Given has been already used."""
class StepDefinitionNotFoundError(Exception):
"""Step definition not found."""
class InvalidStepParserError(Exception):
"""Invalid step parser."""

125
pytest_bdd/parsers.py Normal file
View File

@ -0,0 +1,125 @@
"""Step parsers."""
from __future__ import absolute_import
import re as base_re
import parse as base_parse
from parse_type import cfparse as base_cfparse
import pytest
import six
from .exceptions import InvalidStepParserError
RE_TYPE = type(base_re.compile(''))
class StepParser(object):
"""Parser of the individual step."""
def __init__(self, name):
self.name = name
def parse_arguments(self, name):
"""Get step arguments from the given step name.
:return: `dict` of step arguments
"""
raise NotImplementedError()
def is_matching(self, name):
"""Match given name with the step name."""
raise NotImplementedError()
class re(StepParser):
"""Regex step parser."""
def __init__(self, name, **kwargs):
"""Compile regex."""
super(re, self).__init__(name)
self.regex = base_re.compile(self.name, **kwargs)
def parse_arguments(self, name):
"""Get step arguments.
:return: `dict` of step arguments
"""
return self.regex.match(name).groupdict()
def is_matching(self, name):
"""Match given name with the step name."""
return bool(self.regex.match(name))
class parse(StepParser):
"""parse step parser."""
def __init__(self, name, **kwargs):
"""Compile parse expression."""
super(parse, self).__init__(name)
self.parser = base_parse.compile(self.name, **kwargs)
def parse_arguments(self, name):
"""Get step arguments.
:return: `dict` of step arguments
"""
return self.parser.parse(name).named
def is_matching(self, name):
"""Match given name with the step name."""
try:
return bool(self.parser.parse(name))
except ValueError:
return False
class cfparse(parse):
"""cfparse step parser."""
def __init__(self, name, **kwargs):
"""Compile parse expression."""
super(parse, self).__init__(name)
self.parser = base_cfparse.Parser(self.name, **kwargs)
class string(StepParser):
"""Exact string step parser."""
def parse_arguments(self, name):
"""No parameters are available for simple string step.
:return: `dict` of step arguments
"""
return {}
def is_matching(self, name):
"""Match given name with the step name."""
return self.name == name
def get_parser(step_name):
"""Get parser by given name.
:param step_name: name of the step to parse
:return: step parser object
:rtype: StepArgumentParser
"""
if isinstance(step_name, RE_TYPE):
# backwards compartibility
pytest.config.warn('bdd', "Direct usage of regex is deprecated. Please use pytest_bdd.parsers.re instead.")
return re(step_name.pattern, flags=step_name.flags)
elif isinstance(step_name, six.string_types):
return string(step_name)
elif not hasattr(step_name, 'is_matching') or not hasattr(step_name, 'parse_arguments'):
raise InvalidStepParserError(step_name)
else:
return step_name

View File

@ -16,6 +16,7 @@ import os
import pytest
from _pytest import python
import six
from . import exceptions
from . import fixtures
@ -28,13 +29,12 @@ from .steps import (
execute,
get_caller_function,
get_caller_module,
PY3,
recreate_function,
)
from .types import GIVEN
if PY3:
if six.PY3:
import runpy
execfile = runpy.run_path
@ -79,19 +79,25 @@ def _inject_fixture(request, arg, value):
def find_argumented_step_fixture_name(name, fixturemanager, request=None):
"""Find argumented step fixture name."""
for fixturename, fixturedefs in fixturemanager._arg2fixturedefs.items():
# happens to be that _arg2fixturedefs is changed during the iteration so we use a copy
for fixturename, fixturedefs in list(fixturemanager._arg2fixturedefs.items()):
for fixturedef in fixturedefs:
pattern = getattr(fixturedef.func, "pattern", None)
match = pattern.match(name) if pattern else None
parser = getattr(fixturedef.func, "parser", None)
match = parser.is_matching(name) if parser else None
if match:
converters = getattr(fixturedef.func, "converters", {})
for arg, value in match.groupdict().items():
for arg, value in parser.parse_arguments(name).items():
if arg in converters:
value = converters[arg](value)
if request:
_inject_fixture(request, arg, value)
return force_encode(pattern.pattern)
parser_name = force_encode(parser.name)
if request:
try:
request.getfuncargvalue(parser_name)
except python.FixtureLookupError:
continue
return parser_name
def _find_step_function(request, step, scenario, encoding):

View File

@ -32,24 +32,19 @@ given("I have a beautiful article", fixture="article")
"""
from __future__ import absolute_import
import re
from types import CodeType
import inspect
import sys
import pytest
import six
from .feature import parse_line, force_encode
from .types import GIVEN, WHEN, THEN
PY3 = sys.version_info[0] >= 3
class StepError(Exception):
"""Step declaration error."""
RE_TYPE = type(re.compile(""))
from .exceptions import (
StepError,
)
from .parsers import get_parser
def given(name, fixture=None, converters=None):
@ -59,7 +54,8 @@ def given(name, fixture=None, converters=None):
: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>}.
:param parser: name of the step parser to use
:param parser_args: optional `dict` of arguments to pass to step parser
:raises: StepError in case of wrong configuration.
:note: Can't be used as a decorator when the fixture is specified.
"""
@ -85,6 +81,8 @@ def when(name, converters=None):
:param name: Step name.
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:param parser: name of the step parser to use
:param parser_args: optional `dict` of arguments to pass to step parser
:raises: StepError in case of wrong configuration.
"""
@ -97,6 +95,8 @@ def then(name, converters=None):
:param name: Step name.
:param converters: Optional `dict` of the argument or parameter converters in form
{<param_name>: <converter function>}.
:param parser: name of the step parser to use
:param parser_args: optional `dict` of arguments to pass to step parser
:raises: StepError in case of wrong configuration.
"""
@ -118,6 +118,8 @@ def _step_decorator(step_type, step_name, converters=None):
:param str step_type: Step type (GIVEN, WHEN or THEN).
:param str step_name: Step name as in the feature file.
:param str parser: name of the step parser to use
:param dict parser_args: optional `dict` of arguments to pass to step parser
:return: Decorator function for the step.
@ -126,13 +128,10 @@ def _step_decorator(step_type, step_name, converters=None):
:note: If the step type is GIVEN it will automatically apply the pytest
fixture decorator to the step function.
"""
pattern = None
if isinstance(step_name, RE_TYPE):
pattern = step_name
step_name = pattern.pattern
def decorator(func):
step_func = func
parser_instance = get_parser(step_name)
parsed_step_name = parser_instance.name
if step_type == GIVEN:
if not hasattr(func, "_pytestfixturefunction"):
@ -142,23 +141,25 @@ def _step_decorator(step_type, step_name, converters=None):
step_func.__doc__ = func.__doc__
step_func.fixture = func.__name__
step_func.__name__ = force_encode(step_name)
step_func.step_type = step_type
step_func.converters = converters
step_func.__name__ = force_encode(parsed_step_name)
@pytest.fixture
def lazy_step_func():
return step_func
step_func.step_type = step_type
lazy_step_func = contribute_to_module(get_caller_module(), parsed_step_name, lazy_step_func)
lazy_step_func.step_type = step_type
# Preserve the docstring
lazy_step_func.__doc__ = func.__doc__
if pattern:
lazy_step_func.pattern = pattern
step_func.parser = lazy_step_func.parser = parser_instance
if converters:
lazy_step_func.converters = converters
step_func.converters = lazy_step_func.converters = converters
contribute_to_module(get_caller_module(), step_name, lazy_step_func)
return func
return decorator
@ -174,10 +175,10 @@ def recreate_function(func, module=None, name=None, add_args=[], firstlineno=Non
:return: Function copy.
"""
def get_code(func):
return func.__code__ if PY3 else func.func_code
return func.__code__ if six.PY3 else func.func_code
def set_code(func, code):
if PY3:
if six.PY3:
func.__code__ = code
else:
func.func_code = code
@ -186,7 +187,7 @@ def recreate_function(func, module=None, name=None, add_args=[], firstlineno=Non
"co_argcount", "co_nlocals", "co_stacksize", "co_flags", "co_code", "co_consts", "co_names",
"co_varnames", "co_filename", "co_name", "co_firstlineno", "co_lnotab", "co_freevars", "co_cellvars",
]
if PY3:
if six.PY3:
argnames.insert(1, "co_kwonlyargcount")
for arg in inspect.getargspec(func).args:
@ -222,10 +223,13 @@ def contribute_to_module(module, name, func):
:param module: Module to contribute to.
:param name: Attribute name.
:param func: Function object.
:return: New function copy contributed to the module
"""
name = force_encode(name)
func = recreate_function(func, module=module)
setattr(module, name, func)
return func
def get_caller_module(depth=2):

View File

@ -69,9 +69,12 @@ setup(
] + [("Programming Language :: Python :: %s" % x) for x in "2.6 2.7 3.0 3.1 3.2 3.3 3.4".split()],
cmdclass={"test": ToxTestCommand},
install_requires=[
"pytest>=2.6.0",
"glob2",
"Mako",
"parse",
"parse_type",
"pytest>=2.6.0",
"six",
],
# the following makes a plugin available to py.test
entry_points={

View File

@ -5,7 +5,6 @@ Scenario: Every step takes a parameter with the same name
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

View File

View File

@ -0,0 +1,75 @@
"""Step arguments tests."""
import functools
from pytest_bdd import (
given,
parsers,
scenario,
then,
when,
)
import pytest
from pytest_bdd import exceptions
scenario_when = functools.partial(scenario, '../when_arguments.feature')
scenario_args = functools.partial(scenario, '../args_steps.feature')
@scenario_args('Every step takes a parameter with the same name')
def test_steps():
pass
@scenario_when('Argument in when, step 1')
def test_argument_in_when_step_1():
pass
@scenario_when('Argument in when, step 2')
def test_argument_in_when_step_2():
pass
def test_multiple_given(request):
"""Using the same given fixture raises an error."""
@scenario_args('Using the same given fixture raises an error')
def test():
pass
with pytest.raises(exceptions.GivenAlreadyUsed):
test(request)
@given(parsers.cfparse('I have {euro:d} Euro'))
def i_have(euro, values):
assert euro == values.pop(0)
@when(parsers.cfparse('I pay {euro:d} Euro'))
def i_pay(euro, values, request):
assert euro == values.pop(0)
@then(parsers.cfparse('I should have {euro:d} Euro'))
def i_should_have(euro, values):
assert euro == values.pop(0)
@given(parsers.cfparse('I have an argument {arg:Number}', extra_types=dict(Number=int)))
def argument(arg):
"""I have an argument."""
return dict(arg=arg)
@when(parsers.cfparse('I get argument {arg:d}'))
def get_argument(argument, arg):
"""Getting argument."""
argument['arg'] = arg
@then(parsers.cfparse('My argument should be {arg:d}'))
def assert_that_my_argument_is_arg(argument, arg):
"""Assert that arg from when equals arg."""
assert argument['arg'] == arg

View File

@ -1,7 +1,8 @@
import re
from pytest_bdd import when
"""Step arguments test configuration."""
import pytest
@when(re.compile(r'I append (?P<n>\d+) to the list'))
def append_to_list(results, n):
results.append(int(n))
@pytest.fixture
def values():
"""List to ensure that steps are executed."""
return [1, 2, 1, 0, 999999]

View File

View File

@ -0,0 +1,75 @@
"""Step arguments tests."""
import functools
from pytest_bdd import (
given,
parsers,
scenario,
then,
when,
)
import pytest
from pytest_bdd import exceptions
scenario_when = functools.partial(scenario, '../when_arguments.feature')
scenario_args = functools.partial(scenario, '../args_steps.feature')
@scenario_args('Every step takes a parameter with the same name')
def test_steps():
pass
@scenario_when('Argument in when, step 1')
def test_argument_in_when_step_1():
pass
@scenario_when('Argument in when, step 2')
def test_argument_in_when_step_2():
pass
def test_multiple_given(request):
"""Using the same given fixture raises an error."""
@scenario_args('Using the same given fixture raises an error')
def test():
pass
with pytest.raises(exceptions.GivenAlreadyUsed):
test(request)
@given(parsers.parse('I have {euro:d} Euro'))
def i_have(euro, values):
assert euro == values.pop(0)
@when(parsers.parse('I pay {euro:d} Euro'))
def i_pay(euro, values, request):
assert euro == values.pop(0)
@then(parsers.parse('I should have {euro:d} Euro'))
def i_should_have(euro, values):
assert euro == values.pop(0)
@given(parsers.parse('I have an argument {arg:Number}', extra_types=dict(Number=int)))
def argument(arg):
"""I have an argument."""
return dict(arg=arg)
@when(parsers.parse('I get argument {arg:d}'))
def get_argument(argument, arg):
"""Getting argument."""
argument['arg'] = arg
@then(parsers.parse('My argument should be {arg:d}'))
def assert_that_my_argument_is_arg(argument, arg):
"""Assert that arg from when equals arg."""
assert argument['arg'] == arg

View File

View File

@ -1,77 +1,77 @@
"""Step arguments tests."""
import functools
import re
from pytest_bdd import (
given,
parsers,
scenario,
then,
when,
)
import pytest
from pytest_bdd import scenario, given, when, then
from pytest_bdd import exceptions
scenario_when = functools.partial(scenario, '../when_arguments.feature')
@scenario(
'args_steps.feature',
'Every step takes a parameter with the same name',
)
scenario_args = functools.partial(scenario, '../args_steps.feature')
@scenario_args('Every step takes a parameter with the same name')
def test_steps():
pass
sc = functools.partial(scenario, 'when_arguments.feature')
@sc('Argument in when, step 1')
@scenario_when('Argument in when, step 1')
def test_argument_in_when_step_1():
pass
@sc('Argument in when, step 2')
@scenario_when('Argument in when, step 2')
def test_argument_in_when_step_2():
pass
@pytest.fixture
def values():
return [1, 2, 1, 0, 999999]
def test_multiple_given(request):
"""Using the same given fixture raises an error."""
@scenario_args('Using the same given fixture raises an error')
def test():
pass
with pytest.raises(exceptions.GivenAlreadyUsed):
test(request)
@given(re.compile(r'I have (?P<euro>\d+) Euro'), converters=dict(euro=int))
@given(parsers.re(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'), converters=dict(euro=int))
@when(parsers.re(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))
@then(parsers.re(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():
# test backwards compartibility
@given(re.compile(r'I have an argument (?P<arg>\d+)'))
def argument(arg):
"""I have an argument."""
return dict(arg=1)
return dict(arg=arg)
@when(re.compile('I get argument (?P<arg>\d+)'))
@when(parsers.re(r'I get argument (?P<arg>\d+)'))
def get_argument(argument, arg):
"""Getting argument."""
argument['arg'] = arg
@then(re.compile('My argument should be (?P<arg>\d+)'))
@then(parsers.re(r'My argument should be (?P<arg>\d+)'))
def assert_that_my_argument_is_arg(argument, arg):
"""Assert that arg from when equals arg."""
assert argument['arg'] == arg
def test_multiple_given(request):
"""Using the same given fixture raises an error."""
@scenario(
'args_steps.feature',
'Using the same given fixture raises an error',
)
def test():
pass
with pytest.raises(exceptions.GivenAlreadyUsed):
test(request)

View File

@ -1,4 +1,12 @@
from pytest_bdd import scenario, given, then
"""Test step arguments with complex folder structure."""
from pytest_bdd import (
given,
parsers,
scenario,
then,
when,
)
@scenario(
@ -19,6 +27,11 @@ def results():
return []
@when(parsers.parse('I append {n:d} to the list'))
def append_to_list(results, n):
results.append(n)
@then('foo should have value "foo"')
def foo_is_foo(foo):
assert foo == 'foo'

View File

@ -7,7 +7,7 @@ def test_arg_fixture_mix(testdir):
subdir.join("test_a.py").write(py.code.Source("""
import re
import pytest
from pytest_bdd import scenario, given, then
from pytest_bdd import scenario, given, then, parsers
@pytest.fixture
@ -21,12 +21,12 @@ def test_arg_fixture_mix(testdir):
def test_args():
pass
@given(re.compile(r'foo is "(?P<foo>\w+)"'))
@given(parsers.parse('foo is "{foo}"'))
def foo1(foo):
pass
@then(re.compile(r'foo should be "(?P<foo_value>\w+)"'))
@then(parsers.parse('foo should be "{foo_value}"'))
def foo_should_be(foo, foo_value):
assert foo == foo_value
@ -37,12 +37,12 @@ def test_arg_fixture_mix(testdir):
def test_bar():
pass
@given(re.compile(r'it is all fine'))
@given('it is all fine')
def fine():
return "fine"
@then(re.compile(r'foo should be fine'))
@then('foo should be fine')
def foo_should_be_fine(foo):
assert foo == "fine"
"""))
@ -64,12 +64,12 @@ def test_arg_fixture_mix(testdir):
return "fine"
@given(re.compile(r'it is all fine'))
@given('it is all fine')
def fine():
return "fine"
@then(re.compile(r'foo should be fine'))
@then('foo should be fine')
def foo_should_be(foo):
assert foo == "fine"

View File

@ -1,5 +1,5 @@
Scenario: Argument in when, step 1
Given I have an argument
Given I have an argument 1
When I get argument 5
@ -7,7 +7,7 @@ Scenario: Argument in when, step 1
Scenario: Argument in when, step 2
Given I have an argument
Given I have an argument 1
When I get argument 10

View File

@ -1,15 +1,18 @@
"""Test feature background."""
import re
import pytest
from pytest_bdd import scenario, given, then
from pytest_bdd import (
given,
parsers,
scenario,
then,
)
def test_background_basic(request):
"""Test feature background."""
@scenario(
"background.feature",
"Basic usage",
@ -22,7 +25,6 @@ def test_background_basic(request):
def test_background_check_order(request):
"""Test feature background to ensure that backound steps are executed first."""
@scenario(
"background.feature",
"Background steps are executed first",
@ -38,7 +40,7 @@ def foo():
return {}
@given(re.compile("a background step with multiple lines:\n(?P<data>.+)", re.DOTALL))
@given(parsers.re(r'a background step with multiple lines:\n(?P<data>.+)', flags=re.DOTALL))
def multi_line(foo, data):
assert data == "one\ntwo"

View File

@ -1,11 +1,16 @@
"""Multiline steps tests."""
import re
import textwrap
import pytest
from pytest_bdd import given, then, scenario, exceptions
from pytest_bdd import (
exceptions,
given,
parsers,
scenario,
then,
)
@pytest.mark.parametrize(["feature_text", "expected_text"], [
@ -72,7 +77,7 @@ def test_multiline(request, tmpdir, feature_text, expected_text):
test_multiline(request)
@given(re.compile(r'I have a step with:\n(?P<text>.+)', re.DOTALL))
@given(parsers.re(r'I have a step with:\n(?P<text>.+)', flags=re.DOTALL))
def i_have_text(text):
return text

View File

@ -0,0 +1,17 @@
"""Main command."""
import os
import sys
from pytest_bdd.scripts import main
PATH = os.path.dirname(__file__)
def test_main(monkeypatch, capsys):
"""Test if main commmand shows help when called without the subcommand."""
monkeypatch.setattr(sys, 'argv', ['pytest-bdd'])
monkeypatch.setattr(sys, 'exit', lambda x: x)
main()
out, err = capsys.readouterr()
assert 'usage: pytest-bdd [-h]' in err
assert 'pytest-bdd: error:' in err

View File

@ -2,10 +2,14 @@
"""Tests for testing cases when we have unicode in feature file."""
import sys
import re
import pytest
import functools
from pytest_bdd import scenario, given, then
from pytest_bdd import (
given,
parsers,
scenario,
then,
)
scenario = functools.partial(scenario, 'unicode.feature')
@ -29,7 +33,7 @@ def string():
return {'content': ''}
@given(re.compile(u"у мене є рядок який містить '{0}'".format('(?P<content>.+)')))
@given(parsers.parse(u"у мене є рядок який містить '{content}'"))
def there_is_a_string_with_content(content, string):
"""Create string with unicode content."""
string['content'] = content
@ -47,7 +51,7 @@ def assert_that_the_other_string_equals_to_content(string):
assert string['content'] == u"с каким-то контентом"
@then(re.compile(r"I should see that the string equals to content '(?P<content>.+)'"))
@then(parsers.parse("I should see that the string equals to content '{content}'"))
def assert_that_the_string_equals_to_content(content, string):
"""Assert that the string equals to content."""
assert string['content'] == content