forked from test_framework/pytest-bdd
This commit is contained in:
parent
48336f1f94
commit
29f2c1cbe4
10
.travis.yml
10
.travis.yml
|
@ -1,15 +1,9 @@
|
||||||
language: python
|
language: python
|
||||||
sudo: false
|
sudo: false
|
||||||
# command to install dependencies
|
|
||||||
install:
|
|
||||||
- pip install python-coveralls
|
|
||||||
- pip install -U virtualenv py
|
|
||||||
# # command to run tests
|
# # command to run tests
|
||||||
script: python setup.py test
|
script: make test
|
||||||
after_success:
|
after_success:
|
||||||
- pip install -r requirements-testing.txt -e .
|
- make coveralls
|
||||||
- py.test --cov=pytest_bdd --cov-report=term-missing tests
|
|
||||||
- coveralls
|
|
||||||
notifications:
|
notifications:
|
||||||
email:
|
email:
|
||||||
- bubenkoff@gmail.com
|
- bubenkoff@gmail.com
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
2.7.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Implemented `scenarios` shortcut to automatically bind scenarios to tests (bubenkoff)
|
||||||
|
|
||||||
|
|
||||||
2.6.2
|
2.6.2
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
14
Makefile
14
Makefile
|
@ -1,10 +1,22 @@
|
||||||
# create virtual environment
|
# create virtual environment
|
||||||
|
PATH := .env/bin:$(PATH)
|
||||||
|
|
||||||
.env:
|
.env:
|
||||||
virtualenv .env
|
virtualenv .env
|
||||||
|
|
||||||
# install all needed for development
|
# install all needed for development
|
||||||
develop: .env
|
develop: .env
|
||||||
.env/bin/pip install -e . -r requirements-testing.txt tox
|
pip install -e . -r requirements-testing.txt tox python-coveralls
|
||||||
|
|
||||||
|
coverage: develop
|
||||||
|
coverage run --source=pytest_bdd .env/bin/py.test tests
|
||||||
|
coverage report -m
|
||||||
|
|
||||||
|
test: develop
|
||||||
|
tox
|
||||||
|
|
||||||
|
coveralls: coverage
|
||||||
|
coveralls
|
||||||
|
|
||||||
# clean the development envrironment
|
# clean the development envrironment
|
||||||
clean:
|
clean:
|
||||||
|
|
48
README.rst
48
README.rst
|
@ -330,6 +330,7 @@ Note that `then` step definition (`text_should_be_correct`) in this example uses
|
||||||
by a a `given` step (`i_have_text`) argument with the same name (`text`). This possibility is described in
|
by a a `given` step (`i_have_text`) argument with the same name (`text`). This possibility is described in
|
||||||
the `Step arguments are fixtures as well!`_ section.
|
the `Step arguments are fixtures as well!`_ section.
|
||||||
|
|
||||||
|
|
||||||
Scenario parameters
|
Scenario parameters
|
||||||
-------------------
|
-------------------
|
||||||
Scenario decorator can accept such optional keyword arguments:
|
Scenario decorator can accept such optional keyword arguments:
|
||||||
|
@ -338,6 +339,52 @@ Scenario decorator can accept such optional keyword arguments:
|
||||||
* ``example_converters`` - mapping to pass functions to convert example values provided in feature files.
|
* ``example_converters`` - mapping to pass functions to convert example values provided in feature files.
|
||||||
|
|
||||||
|
|
||||||
|
Scenarios shortcut
|
||||||
|
------------------
|
||||||
|
|
||||||
|
If you have relatively large set of feature files, it's boring to manually bind scenarios to the tests using the
|
||||||
|
scenario decorator. Of course with the manual approach you get all the power to be able to additionally parametrize
|
||||||
|
the test, give the test function a nice name, document it, etc, but in the majority of the cases you don't need that.
|
||||||
|
Instead you want to bind `all` scenarios found in the `feature` folder(s) recursively automatically.
|
||||||
|
For this - there's a `scenarios` helper.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from pytest_bdd import scenarios
|
||||||
|
|
||||||
|
# assume 'features' subfolder is in this file's directory
|
||||||
|
scenarios('features')
|
||||||
|
|
||||||
|
That's all you need to do to bind all scenarios found in the `features` folder!
|
||||||
|
Note that you can pass multiple paths, and those paths can be either feature files or feature folders.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from pytest_bdd import scenarios
|
||||||
|
|
||||||
|
# pass multiple paths/files
|
||||||
|
scenarios('features', 'other_features/some.feature', 'some_other_features')
|
||||||
|
|
||||||
|
But what if you need to manually bind certain scenario, leaving others to be automatically bound?
|
||||||
|
Just write your scenario in a `normal` way, but ensure you do it `BEFORE` the call of `scenarios` helper.
|
||||||
|
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from pytest_bdd import scenario, scenarios
|
||||||
|
|
||||||
|
@scenario('features/some.feature', 'Test something')
|
||||||
|
def test_something():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# assume 'features' subfolder is in this file's directory
|
||||||
|
scenarios('features')
|
||||||
|
|
||||||
|
In the example above `test_something` scenario binding will be kept manual, other scenarios found in the `features`
|
||||||
|
folder will be bound automatically.
|
||||||
|
|
||||||
|
|
||||||
Scenario outlines
|
Scenario outlines
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
@ -377,7 +424,6 @@ pytest-bdd feature file format also supports example tables in different way:
|
||||||
This form allows to have tables with lots of columns keeping the maximum text width predictable without significant
|
This form allows to have tables with lots of columns keeping the maximum text width predictable without significant
|
||||||
readability change.
|
readability change.
|
||||||
|
|
||||||
|
|
||||||
The code will look like:
|
The code will look like:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
"""pytest-bdd public API."""
|
"""pytest-bdd public API."""
|
||||||
|
|
||||||
__version__ = '2.6.2'
|
__version__ = '2.7.0'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pytest_bdd.steps import given, when, then
|
from pytest_bdd.steps import given, when, then
|
||||||
from pytest_bdd.scenario import scenario
|
from pytest_bdd.scenario import scenario, scenarios
|
||||||
|
|
||||||
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__]
|
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, scenarios.__name__]
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# avoid import errors when only __version__ is needed (for setup.py)
|
# avoid import errors when only __version__ is needed (for setup.py)
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -44,3 +44,8 @@ class StepDefinitionNotFoundError(Exception):
|
||||||
class InvalidStepParserError(Exception):
|
class InvalidStepParserError(Exception):
|
||||||
|
|
||||||
"""Invalid step parser."""
|
"""Invalid step parser."""
|
||||||
|
|
||||||
|
|
||||||
|
class NoScenariosFound(Exception):
|
||||||
|
|
||||||
|
"""No scenarios found."""
|
||||||
|
|
|
@ -29,6 +29,8 @@ import re
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
import glob2
|
||||||
|
|
||||||
from . import types
|
from . import types
|
||||||
from . import exceptions
|
from . import exceptions
|
||||||
|
|
||||||
|
@ -149,6 +151,29 @@ def get_tags(line):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_features(paths):
|
||||||
|
"""Get features for given paths.
|
||||||
|
|
||||||
|
:param list paths: `list` of paths (file or dirs)
|
||||||
|
|
||||||
|
:return: `list` of `Feature` objects.
|
||||||
|
"""
|
||||||
|
seen_names = set()
|
||||||
|
features = []
|
||||||
|
for path in paths:
|
||||||
|
if path in seen_names:
|
||||||
|
continue
|
||||||
|
seen_names.add(path)
|
||||||
|
if op.isdir(path):
|
||||||
|
features.extend(get_features(glob2.iglob(op.join(path, "**", "*.feature"))))
|
||||||
|
else:
|
||||||
|
base, name = op.split(path)
|
||||||
|
feature = Feature.get_feature(base, name)
|
||||||
|
features.append(feature)
|
||||||
|
features.sort(key=lambda feature: feature.name or feature.filename)
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
class Feature(object):
|
class Feature(object):
|
||||||
|
|
||||||
"""Feature."""
|
"""Feature."""
|
||||||
|
|
|
@ -2,23 +2,18 @@
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
|
||||||
|
|
||||||
from mako.lookup import TemplateLookup
|
from mako.lookup import TemplateLookup
|
||||||
import glob2
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
from .feature import Feature
|
|
||||||
from .scenario import (
|
from .scenario import (
|
||||||
find_argumented_step_fixture_name,
|
find_argumented_step_fixture_name,
|
||||||
force_encode
|
force_encode,
|
||||||
|
make_python_name,
|
||||||
)
|
)
|
||||||
|
from .feature import get_features
|
||||||
from .types import STEP_TYPES
|
from .types import STEP_TYPES
|
||||||
|
|
||||||
PYTHON_REPLACE_REGEX = re.compile("\W")
|
|
||||||
|
|
||||||
ALPHA_REGEX = re.compile("^\d+_*")
|
|
||||||
|
|
||||||
tw = py.io.TerminalWriter()
|
tw = py.io.TerminalWriter()
|
||||||
|
|
||||||
template_lookup = TemplateLookup(directories=[os.path.join(os.path.dirname(__file__), "templates")])
|
template_lookup = TemplateLookup(directories=[os.path.join(os.path.dirname(__file__), "templates")])
|
||||||
|
@ -51,12 +46,6 @@ def pytest_cmdline_main(config):
|
||||||
return show_missing_code(config)
|
return show_missing_code(config)
|
||||||
|
|
||||||
|
|
||||||
def make_python_name(string):
|
|
||||||
"""Make python attribute name out of a given string."""
|
|
||||||
string = re.sub(PYTHON_REPLACE_REGEX, "", string.replace(" ", "_"))
|
|
||||||
return re.sub(ALPHA_REGEX, "", string).lower()
|
|
||||||
|
|
||||||
|
|
||||||
def generate_code(features, scenarios, steps):
|
def generate_code(features, scenarios, steps):
|
||||||
"""Generate test code for the given filenames."""
|
"""Generate test code for the given filenames."""
|
||||||
grouped_steps = group_steps(steps)
|
grouped_steps = group_steps(steps)
|
||||||
|
@ -134,29 +123,6 @@ def _find_step_fixturedef(fixturemanager, item, name, encoding="utf-8"):
|
||||||
return fixturedefs
|
return fixturedefs
|
||||||
|
|
||||||
|
|
||||||
def get_features(paths):
|
|
||||||
"""Get features for given paths.
|
|
||||||
|
|
||||||
:param list paths: `list` of paths (file or dirs)
|
|
||||||
|
|
||||||
:return: `list` of `Feature` objects.
|
|
||||||
"""
|
|
||||||
seen_names = set()
|
|
||||||
features = []
|
|
||||||
for path in paths:
|
|
||||||
if path in seen_names:
|
|
||||||
continue
|
|
||||||
seen_names.add(path)
|
|
||||||
if os.path.isdir(path):
|
|
||||||
features.extend(get_features(glob2.iglob(os.path.join(path, "**", "*.feature"))))
|
|
||||||
else:
|
|
||||||
base, name = os.path.split(path)
|
|
||||||
feature = Feature.get_feature(base, name)
|
|
||||||
features.append(feature)
|
|
||||||
features.sort(key=lambda feature: feature.name or feature.filename)
|
|
||||||
return features
|
|
||||||
|
|
||||||
|
|
||||||
def parse_feature_files(paths):
|
def parse_feature_files(paths):
|
||||||
"""Parse feature files of given paths.
|
"""Parse feature files of given paths.
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ test_publish_article = scenario(
|
||||||
import collections
|
import collections
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest import python
|
from _pytest import python
|
||||||
|
@ -24,6 +25,7 @@ from .feature import (
|
||||||
Feature,
|
Feature,
|
||||||
force_encode,
|
force_encode,
|
||||||
force_unicode,
|
force_unicode,
|
||||||
|
get_features,
|
||||||
)
|
)
|
||||||
from .steps import (
|
from .steps import (
|
||||||
execute,
|
execute,
|
||||||
|
@ -33,12 +35,15 @@ from .steps import (
|
||||||
)
|
)
|
||||||
from .types import GIVEN
|
from .types import GIVEN
|
||||||
|
|
||||||
|
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
import runpy
|
import runpy
|
||||||
execfile = runpy.run_path
|
execfile = runpy.run_path
|
||||||
|
|
||||||
|
|
||||||
|
PYTHON_REPLACE_REGEX = re.compile("\W")
|
||||||
|
ALPHA_REGEX = re.compile("^\d+_*")
|
||||||
|
|
||||||
|
|
||||||
def _inject_fixture(request, arg, value):
|
def _inject_fixture(request, arg, value):
|
||||||
"""Inject fixture into pytest fixture request.
|
"""Inject fixture into pytest fixture request.
|
||||||
|
|
||||||
|
@ -363,3 +368,57 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
|
||||||
caller_function,
|
caller_function,
|
||||||
encoding,
|
encoding,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_python_name(string):
|
||||||
|
"""Make python attribute name out of a given string."""
|
||||||
|
string = re.sub(PYTHON_REPLACE_REGEX, "", string.replace(" ", "_"))
|
||||||
|
return re.sub(ALPHA_REGEX, "", string).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def get_python_name_generator(name):
|
||||||
|
python_name = make_python_name(name)
|
||||||
|
suffix = ''
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
def get_name():
|
||||||
|
return 'test_{0}{1}'.format(python_name, suffix)
|
||||||
|
while True:
|
||||||
|
yield get_name()
|
||||||
|
index += 1
|
||||||
|
suffix = '_{0}'.format(index)
|
||||||
|
|
||||||
|
|
||||||
|
def scenarios(*feature_paths):
|
||||||
|
"""Parse features from the paths and put all found scenarios in the caller module.
|
||||||
|
|
||||||
|
:param: paths
|
||||||
|
"""
|
||||||
|
frame = inspect.stack()[1]
|
||||||
|
module = inspect.getmodule(frame[0])
|
||||||
|
base_path = get_fixture(module, "pytestbdd_feature_base_dir")
|
||||||
|
abs_feature_paths = []
|
||||||
|
for path in feature_paths:
|
||||||
|
if not os.path.isabs(path):
|
||||||
|
path = os.path.abspath(os.path.join(base_path, path))
|
||||||
|
abs_feature_paths.append(path)
|
||||||
|
found = False
|
||||||
|
|
||||||
|
module_scenarios = frozenset(
|
||||||
|
(attr.__scenario__.feature.filename, attr.__scenario__.name)
|
||||||
|
for name, attr in module.__dict__.items() if hasattr(attr, '__scenario__'))
|
||||||
|
for feature in get_features(abs_feature_paths):
|
||||||
|
for scenario_name, scenario_object in feature.scenarios.items():
|
||||||
|
# skip already bound scenarios
|
||||||
|
if (scenario_object.feature.filename, scenario_name) not in module_scenarios:
|
||||||
|
@scenario(feature.filename, scenario_name)
|
||||||
|
def _scenario():
|
||||||
|
pass
|
||||||
|
for test_name in get_python_name_generator(scenario_name):
|
||||||
|
if test_name not in module.__dict__:
|
||||||
|
# found an unique test name
|
||||||
|
module.__dict__[test_name] = _scenario
|
||||||
|
break
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
raise exceptions.NoScenariosFound(abs_feature_paths)
|
||||||
|
|
|
@ -242,7 +242,7 @@ def get_caller_module(depth=2):
|
||||||
frame = sys._getframe(depth)
|
frame = sys._getframe(depth)
|
||||||
module = inspect.getmodule(frame)
|
module = inspect.getmodule(frame)
|
||||||
if module is None:
|
if module is None:
|
||||||
raise Exception("empty module")
|
return get_caller_module(depth=depth)
|
||||||
return module
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
"""Test scenarios shortcut."""
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
|
||||||
|
def test_scenarios(testdir):
|
||||||
|
"""Test scenarios shortcut."""
|
||||||
|
testdir.makeconftest("""
|
||||||
|
import pytest
|
||||||
|
from pytest_bdd import given
|
||||||
|
|
||||||
|
@given('I have a bar')
|
||||||
|
def i_have_bar():
|
||||||
|
print('bar!')
|
||||||
|
return 'bar'
|
||||||
|
""")
|
||||||
|
features = testdir.mkdir('features')
|
||||||
|
features.join('test.feature').write_text(textwrap.dedent(u"""
|
||||||
|
Scenario: Test scenario
|
||||||
|
Given I have a bar
|
||||||
|
"""), 'utf-8', ensure=True)
|
||||||
|
features.join('subfolder', 'test.feature').write_text(textwrap.dedent(u"""
|
||||||
|
Scenario: Test subfolder scenario
|
||||||
|
Given I have a bar
|
||||||
|
|
||||||
|
Scenario: Test failing subfolder scenario
|
||||||
|
Given I have a failing bar
|
||||||
|
|
||||||
|
Scenario: Test already bound scenario
|
||||||
|
Given I have a bar
|
||||||
|
|
||||||
|
Scenario: Test scenario
|
||||||
|
Given I have a bar
|
||||||
|
"""), 'utf-8', ensure=True)
|
||||||
|
testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
from pytest_bdd import scenarios, scenario
|
||||||
|
|
||||||
|
@scenario('features/subfolder/test.feature', 'Test already bound scenario')
|
||||||
|
def test_already_bound():
|
||||||
|
pass
|
||||||
|
|
||||||
|
scenarios('features')
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest('-v', '-s')
|
||||||
|
result.stdout.fnmatch_lines(['*collected 5 items'])
|
||||||
|
result.stdout.fnmatch_lines(['*test_test_subfolder_scenario *bar!', 'PASSED'])
|
||||||
|
result.stdout.fnmatch_lines(['*test_test_scenario *bar!', 'PASSED'])
|
||||||
|
result.stdout.fnmatch_lines(['*test_test_failing_subfolder_scenario *FAILED'])
|
||||||
|
result.stdout.fnmatch_lines(['*test_already_bound *bar!', 'PASSED'])
|
||||||
|
result.stdout.fnmatch_lines(['*test_test_scenario_1 *bar!', 'PASSED'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_scenarios_none_found(testdir):
|
||||||
|
"""Test scenarios shortcut when no scenarios found."""
|
||||||
|
testdir.makepyfile("""
|
||||||
|
import pytest
|
||||||
|
from pytest_bdd import scenarios
|
||||||
|
|
||||||
|
scenarios('.')
|
||||||
|
""")
|
||||||
|
result = testdir.runpytest('-vv')
|
||||||
|
result.stdout.fnmatch_lines(['*collected 0 items / 1 errors'])
|
||||||
|
result.stdout.fnmatch_lines(['*NoScenariosFound*'])
|
1
tox.ini
1
tox.ini
|
@ -1,6 +1,7 @@
|
||||||
[tox]
|
[tox]
|
||||||
distshare={homedir}/.tox/distshare
|
distshare={homedir}/.tox/distshare
|
||||||
envlist=py26,py27,py27-xdist,py27-pytest-latest,py33,py34
|
envlist=py26,py27,py27-xdist,py27-pytest-latest,py33,py34
|
||||||
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands= py.test pytest_bdd tests --pep8 --junitxml={envlogdir}/junit-{envname}.xml
|
commands= py.test pytest_bdd tests --pep8 --junitxml={envlogdir}/junit-{envname}.xml
|
||||||
|
|
Loading…
Reference in New Issue