forked from test_framework/pytest-bdd
Merge pull request #96 from bubenkoff/pluggable-parsers
Pluggable parsers for step definitions.
This commit is contained in:
commit
8289cdc17a
|
@ -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
|
||||
|
|
122
README.rst
122
README.rst
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
5
setup.py
5
setup.py
|
@ -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={
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue