BDD tests validation/generation helpers
This commit is contained in:
parent
471ae1d28b
commit
47694d0299
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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(
|
||||
|
|
1
setup.py
1
setup.py
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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)" *'])
|
Loading…
Reference in New Issue