forked from test_framework/pytest-bdd
Merge branch 'master' of github.com:olegpidsadnyi/pytest-bdd
This commit is contained in:
commit
7955801852
|
@ -46,3 +46,4 @@ nosetests.xml
|
|||
/include
|
||||
/src
|
||||
/share
|
||||
/local
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
2.0.0
|
||||
-----
|
||||
|
||||
- Pure pytest parametrization for scenario outlines (bubenkoff)
|
||||
- Argumented steps now support converters (transformations) (bubenkoff)
|
||||
- scenario supports only decorator form (bubenkoff)
|
||||
- Code generation refactoring and cleanup (bubenkoff)
|
||||
|
||||
|
||||
1.0.0
|
||||
-----
|
||||
|
||||
|
|
182
README.rst
182
README.rst
|
@ -23,15 +23,17 @@ 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
|
||||
==================
|
||||
------------------
|
||||
|
||||
::
|
||||
|
||||
pip install pytest-bdd
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
-------
|
||||
|
||||
publish_article.feature:
|
||||
|
||||
|
@ -54,7 +56,9 @@ test_publish_article.py:
|
|||
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
|
||||
test_publish = scenario('publish_article.feature', 'Publishing the article')
|
||||
@scenario('publish_article.feature', 'Publishing the article')
|
||||
def test_publish():
|
||||
pass
|
||||
|
||||
|
||||
@given('I have an article')
|
||||
|
@ -83,8 +87,9 @@ test_publish_article.py:
|
|||
article.refresh() # Refresh the object in the SQLAlchemy session
|
||||
assert article.is_published
|
||||
|
||||
|
||||
Step aliases
|
||||
============
|
||||
------------
|
||||
|
||||
Sometimes it is needed to declare the same fixtures or steps with the
|
||||
different names for better readability. In order to use the same step
|
||||
|
@ -115,8 +120,9 @@ default author.
|
|||
Given I'm the admin
|
||||
And there is an article
|
||||
|
||||
|
||||
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.
|
||||
|
@ -143,40 +149,47 @@ The code will look like:
|
|||
import re
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
|
||||
test_arguments = scenario('arguments.feature', 'Arguments for given, when, thens')
|
||||
|
||||
@given(re.compile('there are (?P<start>\d+) cucumbers'))
|
||||
@scenario('arguments.feature', 'Arguments for given, when, thens')
|
||||
def test_arguments():
|
||||
pass
|
||||
|
||||
|
||||
@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 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
|
||||
=================
|
||||
-----------------
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: gherkin
|
||||
|
@ -190,6 +203,24 @@ Example:
|
|||
| start | eat | left |
|
||||
| 12 | 5 | 7 |
|
||||
|
||||
pytest-bdd feature file format also supports example tables in different way:
|
||||
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Scenario Outline: Outlined given, when, thens
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
||||
Examples: Vertical
|
||||
| start | 12 | 2 |
|
||||
| eat | 5 | 1 |
|
||||
| left | 7 | 1 |
|
||||
|
||||
This form allows to have tables with lots of columns keeping the maximum text width predictable without significant
|
||||
readability change.
|
||||
|
||||
|
||||
The code will look like:
|
||||
|
||||
|
@ -198,45 +229,53 @@ The code will look like:
|
|||
from pytest_bdd import given, when, then, scenario
|
||||
|
||||
|
||||
test_outlined = scenario(
|
||||
@scenario(
|
||||
'outline.feature',
|
||||
'Outlined given, when, thens',
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_outlined():
|
||||
pass
|
||||
|
||||
|
||||
@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',
|
||||
)
|
||||
|
@ -266,7 +305,7 @@ The significant downside of this approach is inability to see the test table fro
|
|||
|
||||
|
||||
Test setup
|
||||
==========
|
||||
----------
|
||||
|
||||
Test setup is implemented within the Given section. Even though these steps
|
||||
are executed imperatively to apply possible side-effects, pytest-bdd is trying
|
||||
|
@ -356,7 +395,7 @@ Will raise an exception if the step is using the regular expression pattern.
|
|||
|
||||
|
||||
Reusing fixtures
|
||||
================
|
||||
----------------
|
||||
|
||||
Sometimes scenarios define new names for the fixture that can be
|
||||
inherited. Fixtures can be reused with other names using given():
|
||||
|
@ -367,7 +406,7 @@ inherited. Fixtures can be reused with other names using given():
|
|||
|
||||
|
||||
Reusing steps
|
||||
=============
|
||||
-------------
|
||||
|
||||
It is possible to define some common steps in the parent conftest.py and
|
||||
simply expect them in the child test file.
|
||||
|
@ -400,14 +439,16 @@ test_common.py:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
test_conftest = scenario('common_steps.feature', 'All steps are declared in the conftest')
|
||||
@scenario('common_steps.feature', 'All steps are declared in the conftest')
|
||||
def test_conftest():
|
||||
pass
|
||||
|
||||
There are no definitions of the steps in the test file. They were
|
||||
collected from the parent conftests.
|
||||
|
||||
|
||||
Feature file paths
|
||||
==================
|
||||
------------------
|
||||
|
||||
But default, pytest-bdd will use current module’s path as base path for
|
||||
finding feature files, but this behaviour can be changed by having
|
||||
|
@ -426,11 +467,14 @@ test_publish_article.py:
|
|||
def pytestbdd_feature_base_dir():
|
||||
return '/home/user/projects/foo.bar/features'
|
||||
|
||||
test_publish = scenario('publish_article.feature', 'Publishing the article')
|
||||
|
||||
@scenario('publish_article.feature', 'Publishing the article')
|
||||
def test_publish():
|
||||
pass
|
||||
|
||||
|
||||
Avoid retyping the feature file name
|
||||
====================================
|
||||
------------------------------------
|
||||
|
||||
If you want to avoid retyping the feature file name when defining your scenarios in a test file, use functools.partial.
|
||||
This will make your life much easier when defining multiple scenarios in a test file.
|
||||
|
@ -449,14 +493,22 @@ test_publish_article.py:
|
|||
|
||||
scenario = partial(pytest_bdd.scenario, '/path/to/publish_article.feature')
|
||||
|
||||
test_publish = scenario('Publishing the article')
|
||||
test_publish_unprivileged = scenario('Publishing the article as unprivileged user')
|
||||
|
||||
@scenario('Publishing the article')
|
||||
def test_publish():
|
||||
pass
|
||||
|
||||
|
||||
@scenario('Publishing the article as unprivileged user')
|
||||
def test_publish_unprivileged():
|
||||
pass
|
||||
|
||||
|
||||
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>`_
|
||||
which might be helpful building useful reporting, visualization, etc on top of it:
|
||||
|
@ -476,18 +528,54 @@ 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
|
||||
|
||||
|
||||
|
||||
Migration of your tests from versions 0.x.x-1.x.x
|
||||
-------------------------------------------------
|
||||
|
||||
In version 2.0.0, the backward-incompartible change was introduced: scenario function can now only be used as a
|
||||
decorator. Reasons for that:
|
||||
|
||||
* test code readability is much higher using normal python function syntax;
|
||||
* pytest-bdd internals are much cleaner and shorter when using single approach instead of supporting two;
|
||||
* after moving to parsing-on-import-time approach for feature files, it's not possible to detect whether it's a
|
||||
decorator more or not, so to support it along with functional approach there needed to be special parameter
|
||||
for that, which is also a backward-incompartible change.
|
||||
To help users migrate to newer version, there's migration console script provided with **migrate** extra:
|
||||
|
||||
|
||||
::
|
||||
|
||||
# install extra for migration
|
||||
pip install pytest-bdd[migrate]
|
||||
|
||||
# run migration script
|
||||
pytestbdd_migrate_tests <your test folder>
|
||||
|
||||
Under the hood the script does the replacement from this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
test_function = scenario('publish_article.feature', 'Publishing the article')
|
||||
|
||||
to this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@scenario('publish_article.feature', 'Publishing the article')
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
* pytest-bdd-splinter - collection of fixtures for the real browser BDD testing
|
||||
|
||||
License
|
||||
=======
|
||||
-------
|
||||
|
||||
This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_.
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from pytest_bdd.steps import given, when, then # pragma: no cover
|
||||
from pytest_bdd.scenario import scenario # pragma: no cover
|
||||
|
||||
|
||||
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__] # pragma: no cover
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
"""pytest-bdd Exceptions."""
|
||||
|
||||
|
||||
class ScenarioIsDecoratorOnly(Exception):
|
||||
"""Scenario can be only used as decorator."""
|
||||
|
||||
|
||||
class ScenarioValidationError(Exception):
|
||||
"""Base class for scenario validation."""
|
||||
|
||||
|
||||
class ScenarioNotFound(ScenarioValidationError): # pragma: no cover
|
||||
"""Scenario Not Found"""
|
||||
|
||||
|
||||
class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover
|
||||
"""Scenario steps argumets do not match declared scenario examples."""
|
||||
|
||||
|
||||
class StepTypeError(ScenarioValidationError): # pragma: no cover
|
||||
"""Step definition is not of the type expected in the scenario."""
|
||||
|
||||
|
||||
class GivenAlreadyUsed(ScenarioValidationError): # pragma: no cover
|
||||
"""Fixture that implements the Given has been already used."""
|
|
@ -26,10 +26,8 @@ one line.
|
|||
import re # pragma: no cover
|
||||
import sys # pragma: no cover
|
||||
|
||||
from pytest_bdd.types import (
|
||||
FEATURE, SCENARIO, SCENARIO_OUTLINE, EXAMPLES, EXAMPLES_HEADERS, EXAMPLE_LINE, GIVEN, WHEN,
|
||||
THEN # pragma: no cover
|
||||
)
|
||||
from pytest_bdd import types # pragma: no cover
|
||||
from pytest_bdd import exceptions # pragma: no cover
|
||||
|
||||
|
||||
class FeatureError(Exception): # pragma: no cover
|
||||
|
@ -48,16 +46,17 @@ class FeatureError(Exception): # pragma: no cover
|
|||
features = {} # pragma: no cover
|
||||
|
||||
|
||||
STEP_PREFIXES = { # pragma: no cover
|
||||
'Feature: ': FEATURE,
|
||||
'Scenario Outline: ': SCENARIO_OUTLINE,
|
||||
'Examples:': EXAMPLES,
|
||||
'Scenario: ': SCENARIO,
|
||||
'Given ': GIVEN,
|
||||
'When ': WHEN,
|
||||
'Then ': THEN,
|
||||
'And ': None, # Unknown step type
|
||||
}
|
||||
STEP_PREFIXES = [ # pragma: no cover
|
||||
('Feature: ', types.FEATURE),
|
||||
('Scenario Outline: ', types.SCENARIO_OUTLINE),
|
||||
('Examples: Vertical', types.EXAMPLES_VERTICAL),
|
||||
('Examples:', types.EXAMPLES),
|
||||
('Scenario: ', types.SCENARIO),
|
||||
('Given ', types.GIVEN),
|
||||
('When ', types.WHEN),
|
||||
('Then ', types.THEN),
|
||||
('And ', None), # Unknown step type
|
||||
]
|
||||
|
||||
COMMENT_SYMBOLS = '#' # pragma: no cover
|
||||
|
||||
|
@ -70,9 +69,9 @@ def get_step_type(line):
|
|||
:param line: Line of the Feature file
|
||||
:return: SCENARIO, GIVEN, WHEN, THEN, or `None` if can't be detected.
|
||||
"""
|
||||
for prefix in STEP_PREFIXES:
|
||||
for prefix, _type in STEP_PREFIXES:
|
||||
if line.startswith(prefix):
|
||||
return STEP_PREFIXES[prefix]
|
||||
return _type
|
||||
|
||||
|
||||
def get_step_params(name):
|
||||
|
@ -104,7 +103,7 @@ def remove_prefix(line):
|
|||
:return: Line without the prefix.
|
||||
|
||||
"""
|
||||
for prefix in STEP_PREFIXES:
|
||||
for prefix, _ in STEP_PREFIXES:
|
||||
if line.startswith(prefix):
|
||||
return line[len(prefix):].strip()
|
||||
return line
|
||||
|
@ -156,20 +155,21 @@ class Feature(object):
|
|||
continue
|
||||
mode = get_step_type(line) or mode
|
||||
|
||||
if mode == GIVEN and prev_mode not in (GIVEN, SCENARIO, SCENARIO_OUTLINE):
|
||||
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)
|
||||
|
||||
if mode == WHEN and prev_mode not in (SCENARIO, SCENARIO_OUTLINE, GIVEN, WHEN):
|
||||
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)
|
||||
|
||||
if mode == THEN and prev_mode not in (GIVEN, WHEN, THEN):
|
||||
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)
|
||||
|
||||
if mode == FEATURE:
|
||||
if prev_mode != FEATURE:
|
||||
if mode == types.FEATURE:
|
||||
if prev_mode != types.FEATURE:
|
||||
self.name = remove_prefix(line)
|
||||
else:
|
||||
description.append(line)
|
||||
|
@ -178,16 +178,21 @@ class Feature(object):
|
|||
|
||||
# Remove Feature, Given, When, Then, And
|
||||
line = remove_prefix(line)
|
||||
if mode in [SCENARIO, SCENARIO_OUTLINE]:
|
||||
self.scenarios[line] = scenario = Scenario(line)
|
||||
elif mode == EXAMPLES:
|
||||
mode = EXAMPLES_HEADERS
|
||||
elif mode == EXAMPLES_HEADERS:
|
||||
if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]:
|
||||
self.scenarios[line] = scenario = Scenario(self, line)
|
||||
elif mode == types.EXAMPLES:
|
||||
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('|') if l.strip()])
|
||||
mode = EXAMPLE_LINE
|
||||
elif mode == EXAMPLE_LINE:
|
||||
mode = types.EXAMPLE_LINE
|
||||
elif mode == types.EXAMPLE_LINE:
|
||||
scenario.add_example([l.strip() for l in line.split('|') if l.strip()])
|
||||
elif mode and mode != FEATURE:
|
||||
elif mode == types.EXAMPLE_LINE_VERTICAL:
|
||||
line = [l.strip() for l in line.split('|') if l.strip()]
|
||||
scenario.add_example_row(line[0], line[1:])
|
||||
elif mode and mode != types.FEATURE:
|
||||
scenario.add_step(step_name=line, step_type=mode)
|
||||
|
||||
self.description = u'\n'.join(description)
|
||||
|
@ -215,12 +220,15 @@ class Feature(object):
|
|||
class Scenario(object):
|
||||
"""Scenario."""
|
||||
|
||||
def __init__(self, name):
|
||||
def __init__(self, feature, name, example_converters=None):
|
||||
self.feature = feature
|
||||
self.name = name
|
||||
self.params = set()
|
||||
self.steps = []
|
||||
self.example_params = []
|
||||
self.examples = []
|
||||
self.vertical_examples = []
|
||||
self.example_converters = example_converters
|
||||
|
||||
def add_step(self, step_name, step_type):
|
||||
"""Add step to the scenario.
|
||||
|
@ -229,8 +237,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 +247,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.
|
||||
|
@ -249,10 +257,63 @@ class Scenario(object):
|
|||
"""
|
||||
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):
|
||||
"""Get scenario pytest parametrization table."""
|
||||
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 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):
|
||||
"""Validate the scenario.
|
||||
|
||||
:raises: `ScenarioValidationError`
|
||||
|
||||
"""
|
||||
if self.params and self.example_params and self.params != set(self.example_params):
|
||||
raise exceptions.ScenarioExamplesNotValidError(
|
||||
"""Scenario "{0}" in the feature "{1}" has not valid examples. """
|
||||
"""Set of step parameters {2} should match set of example values {3}.""".format(
|
||||
self.name, self.feature.filename, sorted(self.params), sorted(self.example_params),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Step(object):
|
||||
"""Step."""
|
||||
|
||||
def __init__(self, name, type):
|
||||
def __init__(self, name, type, params):
|
||||
self.name = name
|
||||
self.type = type
|
||||
self.params = params
|
||||
|
|
|
@ -11,34 +11,24 @@ test_publish_article = scenario(
|
|||
)
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
import os
|
||||
import imp
|
||||
|
||||
import inspect # pragma: no cover
|
||||
from os import path as op # pragma: no cover
|
||||
|
||||
import pytest
|
||||
|
||||
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.steps import execute, recreate_function, get_caller_module, get_caller_function
|
||||
from pytest_bdd.types import GIVEN
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
class ScenarioValidationError(Exception):
|
||||
"""Base class for scenario validation."""
|
||||
|
||||
|
||||
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 StepTypeError(ScenarioValidationError): # pragma: no cover
|
||||
"""Step definition is not of the type expected in the scenario."""
|
||||
|
||||
|
||||
class GivenAlreadyUsed(ScenarioValidationError): # pragma: no cover
|
||||
"""Fixture that implements the Given has been already used."""
|
||||
from pytest_bdd import plugin
|
||||
|
||||
|
||||
def _inject_fixture(request, arg, value):
|
||||
|
@ -99,40 +89,46 @@ def _find_step_function(request, name, encoding):
|
|||
|
||||
pattern = getattr(fixturedef.func, 'pattern', None)
|
||||
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
|
||||
|
||||
|
||||
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),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _execute_scenario_outline(feature, scenario, request, encoding):
|
||||
"""Execute the scenario outline."""
|
||||
for example in scenario.examples:
|
||||
for key, value in dict(zip(scenario.example_params, example)).items():
|
||||
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)
|
||||
|
||||
givens = set()
|
||||
# Execute scenario steps
|
||||
for step in scenario.steps:
|
||||
|
@ -146,87 +142,128 @@ def _execute_scenario(feature, scenario, request, encoding):
|
|||
try:
|
||||
# Check the step types are called in the correct order
|
||||
if step_func.step_type != step.type:
|
||||
raise StepTypeError(
|
||||
raise exceptions.StepTypeError(
|
||||
'Wrong step type "{0}" while "{1}" is expected.'.format(step_func.step_type, step.type)
|
||||
)
|
||||
|
||||
# Check if the fixture that implements given step has not been yet used by another given step
|
||||
if step.type == GIVEN:
|
||||
if step_func.fixture in givens:
|
||||
raise GivenAlreadyUsed(
|
||||
raise exceptions.GivenAlreadyUsed(
|
||||
'Fixture "{0}" that implements this "{1}" given step has been already used.'.format(
|
||||
step_func.fixture, step.name,
|
||||
)
|
||||
)
|
||||
givens.add(step_func.fixture)
|
||||
except ScenarioValidationError as exception:
|
||||
except exceptions.ScenarioValidationError as exception:
|
||||
request.config.hook.pytest_bdd_step_validation_error(
|
||||
request=request, feature=feature, scenario=scenario, step=step, step_func=step_func,
|
||||
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 _get_scenario_decorator(
|
||||
feature, feature_name, scenario, scenario_name, caller_module, caller_function, encoding):
|
||||
"""Get scenario decorator."""
|
||||
g = locals()
|
||||
g['_execute_scenario'] = _execute_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)
|
||||
def decorator(_pytestbdd_function):
|
||||
if isinstance(_pytestbdd_function, python.FixtureRequest):
|
||||
raise exceptions.ScenarioIsDecoratorOnly(
|
||||
'scenario function can only be used as a decorator. Refer to the documentation.')
|
||||
|
||||
g.update(locals())
|
||||
|
||||
args = inspect.getargspec(_pytestbdd_function).args
|
||||
function_args = list(args)
|
||||
for arg in scenario.example_params:
|
||||
if arg not in function_args:
|
||||
function_args.append(arg)
|
||||
if 'request' not in function_args:
|
||||
function_args.append('request')
|
||||
|
||||
code = """def {name}({function_args}):
|
||||
_execute_scenario(feature, scenario, request, encoding)
|
||||
_pytestbdd_function({args})""".format(
|
||||
name=_pytestbdd_function.__name__,
|
||||
function_args=', '.join(function_args),
|
||||
args=', '.join(args))
|
||||
|
||||
execute(code, g)
|
||||
|
||||
_scenario = recreate_function(
|
||||
g[_pytestbdd_function.__name__], module=caller_module, firstlineno=caller_function.f_lineno)
|
||||
|
||||
params = scenario.get_params()
|
||||
|
||||
if params:
|
||||
_scenario = pytest.mark.parametrize(*params)(_scenario)
|
||||
|
||||
_scenario.__doc__ = '{feature_name}: {scenario_name}'.format(
|
||||
feature_name=feature_name, scenario_name=scenario_name)
|
||||
return _scenario
|
||||
|
||||
decorator = recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno)
|
||||
return recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno)
|
||||
|
||||
return decorator
|
||||
|
||||
def scenario(
|
||||
feature_name, scenario_name, encoding='utf-8', example_converters=None,
|
||||
caller_module=None, caller_function=None):
|
||||
"""Scenario."""
|
||||
|
||||
caller_module = caller_module or get_caller_module()
|
||||
caller_function = caller_function or get_caller_function()
|
||||
|
||||
# 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)
|
||||
|
||||
# Get the scenario
|
||||
try:
|
||||
scenario = feature.scenarios[scenario_name]
|
||||
except KeyError:
|
||||
raise exceptions.ScenarioNotFound(
|
||||
'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name)
|
||||
)
|
||||
|
||||
scenario.example_converters = example_converters
|
||||
|
||||
# Validate the scenario
|
||||
scenario.validate()
|
||||
|
||||
return _get_scenario_decorator(
|
||||
feature, feature_name, scenario, scenario_name, caller_module, caller_function, encoding)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
"""pytest-bdd scripts."""
|
||||
import glob2
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
MIGRATE_REGEX = re.compile(r'\s?(\w+)\s\=\sscenario\((.+)\)', flags=re.MULTILINE)
|
||||
|
||||
|
||||
def migrate_tests():
|
||||
"""Migrate outdated tests to the most recent form."""
|
||||
if len(sys.argv) != 2:
|
||||
print 'Usage: pytestbdd_migrate_tests <path>'
|
||||
sys.exit(1)
|
||||
path = sys.argv[1]
|
||||
for file_path in glob2.iglob(os.path.join(os.path.abspath(path), '**', '*.py')):
|
||||
migrate_tests_in_file(file_path)
|
||||
|
||||
|
||||
def migrate_tests_in_file(file_path):
|
||||
"""Migrate all bdd-based tests in the given test file."""
|
||||
try:
|
||||
with open(file_path, 'r+') as fd:
|
||||
content = fd.read()
|
||||
new_content = MIGRATE_REGEX.sub(r'\n\n@scenario(\2)\ndef \1():\n pass\n', content)
|
||||
if new_content != content:
|
||||
fd.seek(0)
|
||||
fd.write(new_content)
|
||||
print('migrated: {0}'.format(file_path))
|
||||
else:
|
||||
print('skipped: {0}'.format(file_path))
|
||||
except IOError:
|
||||
pass
|
|
@ -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:
|
||||
|
@ -220,7 +234,6 @@ def contribute_to_module(module, name, func):
|
|||
|
||||
"""
|
||||
func = recreate_function(func, module=module)
|
||||
|
||||
setattr(module, name, func)
|
||||
|
||||
|
||||
|
@ -233,3 +246,7 @@ def get_caller_module(depth=2):
|
|||
def get_caller_function(depth=2):
|
||||
"""Return caller function."""
|
||||
return sys._getframe(depth)
|
||||
|
||||
|
||||
def execute(code, g):
|
||||
exec(code, g)
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
FEATURE = 'feature' # pragma: no cover
|
||||
SCENARIO_OUTLINE = 'scenario outline' # pragma: no cover
|
||||
EXAMPLES = 'examples' # pragma: no cover
|
||||
EXAMPLES_VERTICAL = 'examples vertical' # pragma: no cover
|
||||
EXAMPLES_HEADERS = 'example headers' # pragma: no cover
|
||||
EXAMPLE_LINE = 'example line' # pragma: no cover
|
||||
EXAMPLE_LINE_VERTICAL = 'example line vertical' # pragma: no cover
|
||||
SCENARIO = 'scenario' # pragma: no cover
|
||||
GIVEN = 'given' # pragma: no cover
|
||||
WHEN = 'when' # pragma: no cover
|
||||
|
|
8
setup.py
8
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):
|
||||
|
@ -59,8 +59,14 @@ setup(
|
|||
entry_points={
|
||||
'pytest11': [
|
||||
'pytest-bdd = pytest_bdd.plugin',
|
||||
],
|
||||
'console_scripts': [
|
||||
'pytestbdd_migrate_tests = pytest_bdd.scripts:migrate_tests [migrate]'
|
||||
]
|
||||
},
|
||||
tests_require=['detox'],
|
||||
extras_require={
|
||||
'migrate': ['glob2']
|
||||
},
|
||||
packages=['pytest_bdd'],
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
from pytest_bdd import scenario, given, then
|
||||
|
||||
|
||||
test_steps = scenario(
|
||||
@scenario(
|
||||
'args.feature',
|
||||
'Executed with steps matching step definitons with arguments',
|
||||
)
|
||||
def test_steps():
|
||||
pass
|
||||
|
||||
|
||||
@given('I have a foo fixture with value "foo"')
|
||||
|
|
|
@ -14,12 +14,12 @@ def test_arg_fixture_mix(testdir):
|
|||
def foo():
|
||||
return "fine"
|
||||
|
||||
|
||||
test_args = scenario(
|
||||
@scenario(
|
||||
'arg_and_fixture_mix.feature',
|
||||
'Use the step argument with the same name as fixture of another test',
|
||||
)
|
||||
|
||||
def test_args():
|
||||
pass
|
||||
|
||||
@given(re.compile(r'foo is "(?P<foo>\w+)"'))
|
||||
def foo1(foo):
|
||||
|
@ -30,12 +30,12 @@ def test_arg_fixture_mix(testdir):
|
|||
def foo_should_be(foo, foo_value):
|
||||
assert foo == foo_value
|
||||
|
||||
|
||||
test_bar = scenario(
|
||||
@scenario(
|
||||
'arg_and_fixture_mix.feature',
|
||||
'Everything is fine',
|
||||
)
|
||||
|
||||
def test_bar():
|
||||
pass
|
||||
|
||||
@given(re.compile(r'it is all fine'))
|
||||
def fine():
|
||||
|
@ -52,12 +52,12 @@ def test_arg_fixture_mix(testdir):
|
|||
import pytest
|
||||
from pytest_bdd import scenario, given, then
|
||||
|
||||
|
||||
test_args = scenario(
|
||||
@scenario(
|
||||
'arg_and_fixture_mix.feature',
|
||||
'Everything is fine',
|
||||
)
|
||||
|
||||
def test_args():
|
||||
pass
|
||||
|
||||
@pytest.fixture
|
||||
def foo():
|
||||
|
|
|
@ -4,34 +4,49 @@ import functools
|
|||
import re
|
||||
import pytest
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.scenario import GivenAlreadyUsed
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
test_steps = scenario(
|
||||
@scenario(
|
||||
'args_steps.feature',
|
||||
'Every step takes a parameter with the same name',
|
||||
)
|
||||
def test_steps():
|
||||
pass
|
||||
|
||||
sc = functools.partial(scenario, 'when_arguments.feature')
|
||||
test_argument_in_when_step_1 = sc('Argument in when, step 1')
|
||||
test_argument_in_when_step_2 = sc('Argument in when, step 2')
|
||||
|
||||
|
||||
@sc('Argument in when, step 1')
|
||||
def test_argument_in_when_step_1():
|
||||
pass
|
||||
|
||||
|
||||
@sc('Argument in when, step 2')
|
||||
def test_argument_in_when_step_2():
|
||||
pass
|
||||
|
||||
|
||||
@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 +59,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."""
|
||||
|
@ -57,9 +67,11 @@ def assert_that_my_argument_is_arg(argument, arg):
|
|||
|
||||
def test_multiple_given(request):
|
||||
"""Using the same given fixture raises an error."""
|
||||
test = scenario(
|
||||
@scenario(
|
||||
'args_steps.feature',
|
||||
'Using the same given fixture raises an error',
|
||||
)
|
||||
with pytest.raises(GivenAlreadyUsed):
|
||||
def test():
|
||||
pass
|
||||
with pytest.raises(exceptions.GivenAlreadyUsed):
|
||||
test(request)
|
||||
|
|
|
@ -6,3 +6,36 @@ Scenario Outline: Outlined given, when, thens
|
|||
Examples:
|
||||
| start | eat | left |
|
||||
| 12 | 5 | 7 |
|
||||
| 5 | 4 | 1 |
|
||||
|
||||
|
||||
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 |
|
||||
|
||||
|
||||
Scenario Outline: Outlined with vertical example table
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
||||
Examples: Vertical
|
||||
| start | 12 | 2 |
|
||||
| eat | 5 | 1 |
|
||||
| left | 7 | 1 |
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
from pytest_bdd import scenario, given, when, then
|
||||
|
||||
|
||||
test_steps = scenario('alias.feature', 'Multiple given alias is not evaluated multiple times')
|
||||
@scenario('alias.feature', 'Multiple given alias is not evaluated multiple times')
|
||||
def test_steps():
|
||||
pass
|
||||
|
||||
|
||||
@given('I have an empty list')
|
||||
|
|
|
@ -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,81 @@
|
|||
"""Scenario Outline tests."""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
test_outlined = scenario(
|
||||
@scenario(
|
||||
'outline.feature',
|
||||
'Outlined given, when, thens',
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_outlined():
|
||||
assert test_outlined.parametrize.args == (
|
||||
[u'start', u'eat', u'left'], [[12, 5.0, '7'], [5, 4.0, '1']])
|
||||
|
||||
|
||||
@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."""
|
||||
|
||||
with pytest.raises(exceptions.ScenarioExamplesNotValidError) as exc:
|
||||
@scenario(
|
||||
'outline.feature',
|
||||
'Outlined with wrong examples',
|
||||
)
|
||||
def wrongly_outlined():
|
||||
pass
|
||||
|
||||
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]
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(params=[1, 2, 3])
|
||||
def other_fixture(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@scenario(
|
||||
'outline.feature',
|
||||
'Outlined given, when, thens',
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_outlined_with_other_fixtures(other_fixture):
|
||||
"""Test outlined scenario also using other parametrized fixture."""
|
||||
|
||||
|
||||
@scenario(
|
||||
'outline.feature',
|
||||
'Outlined with vertical example table',
|
||||
example_converters=dict(start=int, eat=float, left=str)
|
||||
)
|
||||
def test_vertical_example():
|
||||
"""Test outlined scenario with vertical examples table."""
|
||||
assert test_vertical_example.parametrize.args == (
|
||||
[u'start', u'eat', u'left'], [[12, 5.0, '7'], [2, 1.0, '1']])
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import pytest
|
||||
|
||||
from pytest_bdd.scenario import NotEnoughScenarioParams
|
||||
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
|
||||
|
||||
|
@ -18,7 +16,7 @@ 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(
|
||||
|
@ -29,26 +27,7 @@ def foo_bar(request):
|
|||
'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')
|
||||
|
|
|
@ -2,10 +2,13 @@ from pytest_bdd.steps import when
|
|||
|
||||
from pytest_bdd import given, then, scenario
|
||||
|
||||
test_reuse = scenario(
|
||||
|
||||
@scenario(
|
||||
'reuse.feature',
|
||||
'Given and when using the same fixture should not evaluate it twice',
|
||||
)
|
||||
def test_reuse():
|
||||
pass
|
||||
|
||||
|
||||
@given('I have an empty list')
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
"""Test scenario decorator."""
|
||||
import pytest
|
||||
|
||||
from pytest_bdd import scenario
|
||||
from pytest_bdd.scenario import ScenarioNotFound
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
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)
|
||||
with pytest.raises(exceptions.ScenarioNotFound):
|
||||
scenario(
|
||||
'not_found.feature',
|
||||
'NOT FOUND'
|
||||
)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import pytest
|
||||
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.scenario import GivenAlreadyUsed
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
test_steps = scenario('steps.feature', 'Executed step by step')
|
||||
@scenario('steps.feature', 'Executed step by step')
|
||||
def test_steps():
|
||||
pass
|
||||
|
||||
|
||||
@given('I have a foo fixture with value "foo"')
|
||||
|
@ -42,7 +44,9 @@ def check_results(results):
|
|||
assert results == [1, 2, 3]
|
||||
|
||||
|
||||
test_when_first = scenario('steps.feature', 'When step can be the first')
|
||||
@scenario('steps.feature', 'When step can be the first')
|
||||
def test_when_first():
|
||||
pass
|
||||
|
||||
|
||||
@when('I do nothing')
|
||||
|
@ -55,7 +59,9 @@ def no_errors():
|
|||
assert True
|
||||
|
||||
|
||||
test_then_after_given = scenario('steps.feature', 'Then step can follow Given step')
|
||||
@scenario('steps.feature', 'Then step can follow Given step')
|
||||
def test_then_after_given():
|
||||
pass
|
||||
|
||||
|
||||
@given('xyz')
|
||||
|
@ -63,16 +69,22 @@ def xyz():
|
|||
"""Used in the test_same_step_name."""
|
||||
return
|
||||
|
||||
test_conftest = scenario('steps.feature', 'All steps are declared in the conftest')
|
||||
|
||||
@scenario('steps.feature', 'All steps are declared in the conftest')
|
||||
def test_conftest():
|
||||
pass
|
||||
|
||||
|
||||
def test_multiple_given(request):
|
||||
"""Using the same given fixture raises an error."""
|
||||
test = scenario(
|
||||
@scenario(
|
||||
'steps.feature',
|
||||
'Using the same given fixture raises an error',
|
||||
)
|
||||
with pytest.raises(GivenAlreadyUsed):
|
||||
def test():
|
||||
pass
|
||||
|
||||
with pytest.raises(exceptions.GivenAlreadyUsed):
|
||||
test(request)
|
||||
|
||||
|
||||
|
@ -195,19 +207,25 @@ def test_step_trace(testdir):
|
|||
def when_it_fails():
|
||||
raise Exception('when fails')
|
||||
|
||||
test_when_fails_inline = scenario('test.feature', 'When step has failure')
|
||||
@scenario('test.feature', 'When step has failure')
|
||||
def test_when_fails_inline():
|
||||
pass
|
||||
|
||||
@scenario('test.feature', 'When step has failure')
|
||||
def test_when_fails_decorated():
|
||||
pass
|
||||
|
||||
test_when_not_found = scenario('test.feature', 'When step is not found')
|
||||
@scenario('test.feature', 'When step is not found')
|
||||
def test_when_not_found():
|
||||
pass
|
||||
|
||||
@when('foo')
|
||||
def foo():
|
||||
return 'foo'
|
||||
|
||||
test_when_step_validation_error = scenario('test.feature', 'When step validation error happens')
|
||||
@scenario('test.feature', 'When step validation error happens')
|
||||
def test_when_step_validation_error():
|
||||
pass
|
||||
""")
|
||||
result = testdir.runpytest('-k test_when_fails_inline', '-vv')
|
||||
assert result.ret == 1
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
"""Test wrong feature syntax."""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.feature import FeatureError
|
||||
from pytest_bdd.scenario import StepTypeError
|
||||
from pytest_bdd import exceptions
|
||||
|
||||
|
||||
@given('something')
|
||||
|
@ -33,9 +35,10 @@ 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)
|
||||
def test_scenario():
|
||||
pass
|
||||
# TODO: assert the exception args from parameters
|
||||
|
||||
|
||||
|
@ -52,17 +55,19 @@ def test_wrong(request, feature, scenario_name):
|
|||
)
|
||||
def test_wrong_type_order(request, scenario_name):
|
||||
"""Test wrong step type order."""
|
||||
sc = scenario('wrong_type_order.feature', scenario_name)
|
||||
with pytest.raises(StepTypeError) as excinfo:
|
||||
sc(request)
|
||||
excinfo # TODO: assert the exception args from parameters
|
||||
@scenario('wrong_type_order.feature', scenario_name)
|
||||
def test_wrong_type_order(request):
|
||||
pass
|
||||
|
||||
with pytest.raises(exceptions.StepTypeError) as excinfo:
|
||||
test_wrong_type_order(request)
|
||||
assert re.match(r'Wrong step type \"(\w+)\" while \"(\w+)\" is expected\.', excinfo.value.args[0])
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -11,9 +11,15 @@ def foo():
|
|||
given('I have alias for foo', fixture='foo')
|
||||
given('I have an alias to the root fixture', fixture='root')
|
||||
|
||||
test_given_with_fixture = scenario('given.feature', 'Test reusing local fixture')
|
||||
|
||||
test_root_alias = scenario('given.feature', 'Test reusing root fixture')
|
||||
@scenario('given.feature', 'Test reusing local fixture')
|
||||
def test_given_with_fixture():
|
||||
pass
|
||||
|
||||
|
||||
@scenario('given.feature', 'Test reusing root fixture')
|
||||
def test_root_alias():
|
||||
pass
|
||||
|
||||
|
||||
@then('foo should be "foo"')
|
||||
|
|
|
@ -8,8 +8,17 @@ import functools
|
|||
from pytest_bdd import scenario, given, then
|
||||
|
||||
scenario = functools.partial(scenario, 'unicode.feature')
|
||||
test_steps_in_feature_file_have_unicode = scenario('Steps in .feature file have unicode')
|
||||
test_steps_in_py_file_have_unicode = scenario('Steps in .py file have unicode')
|
||||
|
||||
|
||||
@scenario('Steps in .feature file have unicode')
|
||||
def test_steps_in_feature_file_have_unicode():
|
||||
pass
|
||||
|
||||
|
||||
@scenario('Steps in .py file have unicode')
|
||||
def test_steps_in_py_file_have_unicode():
|
||||
pass
|
||||
|
||||
|
||||
pattern = '(?P<content>\'\w+\')'
|
||||
|
||||
|
|
Loading…
Reference in New Issue