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
|
||||
sudo: false
|
||||
# command to install dependencies
|
||||
install:
|
||||
- pip install python-coveralls
|
||||
- pip install -U virtualenv py
|
||||
# # command to run tests
|
||||
script: python setup.py test
|
||||
script: make test
|
||||
after_success:
|
||||
- pip install -r requirements-testing.txt -e .
|
||||
- py.test --cov=pytest_bdd --cov-report=term-missing tests
|
||||
- coveralls
|
||||
- make coveralls
|
||||
notifications:
|
||||
email:
|
||||
- bubenkoff@gmail.com
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
2.7.0
|
||||
-----
|
||||
|
||||
- Implemented `scenarios` shortcut to automatically bind scenarios to tests (bubenkoff)
|
||||
|
||||
|
||||
2.6.2
|
||||
-----
|
||||
|
||||
|
|
14
Makefile
14
Makefile
|
@ -1,10 +1,22 @@
|
|||
# create virtual environment
|
||||
PATH := .env/bin:$(PATH)
|
||||
|
||||
.env:
|
||||
virtualenv .env
|
||||
|
||||
# install all needed for development
|
||||
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:
|
||||
|
|
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
|
||||
the `Step arguments are fixtures as well!`_ section.
|
||||
|
||||
|
||||
Scenario parameters
|
||||
-------------------
|
||||
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.
|
||||
|
||||
|
||||
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
|
||||
-----------------
|
||||
|
||||
|
@ -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
|
||||
readability change.
|
||||
|
||||
|
||||
The code will look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
"""pytest-bdd public API."""
|
||||
|
||||
__version__ = '2.6.2'
|
||||
__version__ = '2.7.0'
|
||||
|
||||
try:
|
||||
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:
|
||||
# avoid import errors when only __version__ is needed (for setup.py)
|
||||
pass
|
||||
|
|
|
@ -44,3 +44,8 @@ class StepDefinitionNotFoundError(Exception):
|
|||
class InvalidStepParserError(Exception):
|
||||
|
||||
"""Invalid step parser."""
|
||||
|
||||
|
||||
class NoScenariosFound(Exception):
|
||||
|
||||
"""No scenarios found."""
|
||||
|
|
|
@ -29,6 +29,8 @@ import re
|
|||
import sys
|
||||
import textwrap
|
||||
|
||||
import glob2
|
||||
|
||||
from . import types
|
||||
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):
|
||||
|
||||
"""Feature."""
|
||||
|
|
|
@ -2,23 +2,18 @@
|
|||
|
||||
import itertools
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
import glob2
|
||||
import py
|
||||
|
||||
from .feature import Feature
|
||||
from .scenario import (
|
||||
find_argumented_step_fixture_name,
|
||||
force_encode
|
||||
force_encode,
|
||||
make_python_name,
|
||||
)
|
||||
from .feature import get_features
|
||||
from .types import STEP_TYPES
|
||||
|
||||
PYTHON_REPLACE_REGEX = re.compile("\W")
|
||||
|
||||
ALPHA_REGEX = re.compile("^\d+_*")
|
||||
|
||||
tw = py.io.TerminalWriter()
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""Generate test code for the given filenames."""
|
||||
grouped_steps = group_steps(steps)
|
||||
|
@ -134,29 +123,6 @@ def _find_step_fixturedef(fixturemanager, item, name, encoding="utf-8"):
|
|||
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):
|
||||
"""Parse feature files of given paths.
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ test_publish_article = scenario(
|
|||
import collections
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from _pytest import python
|
||||
|
@ -24,6 +25,7 @@ from .feature import (
|
|||
Feature,
|
||||
force_encode,
|
||||
force_unicode,
|
||||
get_features,
|
||||
)
|
||||
from .steps import (
|
||||
execute,
|
||||
|
@ -33,12 +35,15 @@ from .steps import (
|
|||
)
|
||||
from .types import GIVEN
|
||||
|
||||
|
||||
if six.PY3:
|
||||
import runpy
|
||||
execfile = runpy.run_path
|
||||
|
||||
|
||||
PYTHON_REPLACE_REGEX = re.compile("\W")
|
||||
ALPHA_REGEX = re.compile("^\d+_*")
|
||||
|
||||
|
||||
def _inject_fixture(request, arg, value):
|
||||
"""Inject fixture into pytest fixture request.
|
||||
|
||||
|
@ -363,3 +368,57 @@ def scenario(feature_name, scenario_name, encoding="utf-8", example_converters=N
|
|||
caller_function,
|
||||
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)
|
||||
module = inspect.getmodule(frame)
|
||||
if module is None:
|
||||
raise Exception("empty module")
|
||||
return get_caller_module(depth=depth)
|
||||
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*'])
|
Loading…
Reference in New Issue