BDD tests validation/generation helpers

This commit is contained in:
Anatoly Bubenkov 2014-09-20 23:54:39 +00:00
parent 471ae1d28b
commit 47694d0299
8 changed files with 224 additions and 12 deletions

View File

@ -7,6 +7,7 @@ Unreleased
- Better reporting of a not found scenario (bubenkoff)
- Simple test code generation implemented (bubenkoff)
- Correct timing values for cucumber json reporting (bubenkoff)
- BDD tests validation/generation helpers (bubenkoff)
2.4.0

145
pytest_bdd/generation.py Normal file
View File

@ -0,0 +1,145 @@
"""pytest-bdd missing test code generation."""
import os.path
import itertools
import py
from _pytest.python import getlocation
from collections import defaultdict
tw = py.io.TerminalWriter()
verbose = 1
from .feature import Feature
from .scenario import (
_find_argumented_step_fixture_name,
force_encode
)
def pytest_addoption(parser):
"""Add pytest-bdd options."""
group = parser.getgroup("bdd")
group._addoption(
'--generate-missing',
action="store_true", dest="generate_missing",
default=False,
help="Generate missing bdd test code for given feature files")
group._addoption(
'--feature-file',
action="append", dest="features",
help="Feature file(s) to generate missing code for.")
def pytest_cmdline_main(config):
"""Check config option to show missing code."""
if config.option.generate_missing:
return show_missing_code(config)
def show_missing_code(config):
"""Wrap pytest session to show missing code."""
from _pytest.main import wrap_session
return wrap_session(config, _show_missing_code_main)
def print_missing_code(scenarios, steps):
"""Print missing code with TerminalWriter."""
curdir = py.path.local()
scenario = step = None
for scenario in sorted(scenarios, key=lambda scenario: scenario.name):
tw.line()
tw.line(
'Scenario is not bound to any test: "{scenario.name}" in feature "{scenario.feature.name}"'
' in {scenario.feature.filename}'.format(scenario=scenario), red=True)
if scenario:
tw.sep('-', red=True)
for step in sorted(steps, key=lambda step: step.name):
tw.line()
tw.line(
'Step is not defined: "{step.name}" in scenario: "{step.scenario.name}" in feature'
' "{step.scenario.feature.name}" in {step.scenario.feature.filename}'.format(step=step), red=True)
if step:
tw.sep('-', red=True)
# if len(fixtures) > 1:
# fixtures = sorted(fixtures, key=lambda key: key[2])
# for baseid, module, bestrel, fixturedef in fixtures:
# if previous_argname != argname:
# tw.line()
# tw.sep("-", argname)
# previous_argname = argname
# if verbose <= 0 and argname[0] == "_":
# continue
# funcargspec = bestrel
# tw.line(funcargspec)
def _find_step_fixturedef(fixturemanager, item, name, encoding='utf-8'):
"""Find step fixturedef.
:param request: PyTest Item object.
:param step: `Step`.
:return: Step function.
"""
fixturedefs = fixturemanager.getfixturedefs(force_encode(name, encoding), item.nodeid)
if not fixturedefs:
name = _find_argumented_step_fixture_name(name, fixturemanager)
if name:
return _find_step_fixturedef(fixturemanager, item, name, encoding)
else:
return fixturedefs
def _show_missing_code_main(config, session):
"""Preparing fixture duplicates for output."""
session.perform_collect()
fm = session._fixturemanager
features = [Feature.get_feature(*os.path.split(feature_file)) for feature_file in config.option.features]
scenarios = list(itertools.chain.from_iterable(feature.scenarios.values() for feature in features))
steps = list(itertools.chain.from_iterable(scenario.steps for scenario in scenarios))
for item in session.items:
scenario = getattr(item.obj, '__scenario__', None)
if scenario:
if scenario in scenarios:
scenarios.remove(scenario)
for step in scenario.steps:
fixturedefs = _find_step_fixturedef(fm, item, step.name, )
if fixturedefs:
try:
steps.remove(step)
except ValueError:
pass
# assert fixturedefs is not None
# if not fixturedefs:
# continue
# for fixturedef in fixturedefs:
# loc = getlocation(fixturedef.func, curdir)
# fixture = (
# len(fixturedef.baseid),
# fixturedef.func.__module__,
# curdir.bestrelpath(loc),
# fixturedef
# )
# if fixture[2] not in [f[2] for f in available[argname]]:
# available[argname].append(fixture)
for scenario in scenarios:
for step in scenario.steps:
steps.remove(step)
print_missing_code(scenarios, steps)
if scenarios or steps:
session.exitstatus = 100

View File

@ -82,6 +82,11 @@ def pytest_runtest_makereport(item, call, __multicall__):
return rep
def pytest_addoption(parser):
"""Add pytest-bdd options."""
parser.getgroup("bdd", "BDD")
@given('trace')
@when('trace')
@then('trace')

View File

@ -76,6 +76,23 @@ def _inject_fixture(request, arg, value):
request.fixturenames.append(arg)
def _find_argumented_step_fixture_name(name, fixturemanager, request=None):
"""Find argumented step fixture name."""
for fixturename, fixturedefs in fixturemanager._arg2fixturedefs.items():
for fixturedef in fixturedefs:
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)
if request:
_inject_fixture(request, arg, value)
return pattern.pattern
def _find_step_function(request, step, encoding):
"""Match the step defined by the regular expression pattern.
@ -89,18 +106,9 @@ def _find_step_function(request, step, encoding):
return request.getfuncargvalue(force_encode(name, encoding))
except python.FixtureLookupError:
try:
for fixturename, fixturedefs in request._fixturemanager._arg2fixturedefs.items():
for fixturedef in fixturedefs:
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)
name = _find_argumented_step_fixture_name(name, request._fixturemanager, request)
if name:
return request.getfuncargvalue(name)
raise
except python.FixtureLookupError:
raise exceptions.StepDefinitionNotFoundError(

View File

@ -65,6 +65,7 @@ setup(
'pytest11': [
'pytest-bdd = pytest_bdd.plugin',
'pytest-bdd-cucumber-json = pytest_bdd.cucumber_json',
'pytest-bdd-generation = pytest_bdd.generation',
],
'console_scripts': [
'pytest-bdd = pytest_bdd.scripts:main'

View File

View File

@ -0,0 +1,12 @@
Feature: Missing code generation
Scenario: Scenario tests which are already bound to the tests stay as is
Given I have a bar
Scenario: Code is generated for scenarios which are not bound to any tests
Given I have a bar
Scenario: Code is generated for scenario steps which are not yet defined(implemented)
Given I have a custom bar

View File

@ -0,0 +1,40 @@
"""Code generation and assertion tests."""
import os.path
import py
def test_generate_missing(testdir):
tests = testdir.mkpydir('tests')
with open(os.path.join(os.path.dirname(__file__), 'generation.feature')) as fd:
tests.join('generation.feature').write(fd.read())
tests.join("test_foo.py").write(py.code.Source("""
import functools
from pytest_bdd import scenario, given
scenario = functools.partial(scenario, 'generation.feature')
@given('I have a bar')
def i_have_a_bar():
return 'bar'
@scenario('Scenario tests which are already bound to the tests stay as is')
def test_foo():
pass
@scenario('Code is generated for scenario steps which are not yet defined(implemented)')
def test_missing_steps():
pass
"""))
result = testdir.runpytest(
"tests", "--generate-missing", "--feature-file", tests.join('generation.feature').strpath)
result.stdout.fnmatch_lines([
'Scenario is not bound to any test: "Code is generated for scenarios which are not bound to any tests" *'])
result.stdout.fnmatch_lines([
'Step is not defined: "I have a custom bar" in scenario: "Code is generated for scenario steps which are not '
'yet defined(implemented)" *'])