Implement simple code generation command. Closes #32
This commit is contained in:
parent
c21dda176c
commit
57431dc218
|
@ -5,6 +5,7 @@ Unreleased
|
||||||
----------
|
----------
|
||||||
|
|
||||||
- Better reporting of a not found scenario (bubenkoff)
|
- Better reporting of a not found scenario (bubenkoff)
|
||||||
|
- Simple test code generation implemented (bubenkoff)
|
||||||
|
|
||||||
|
|
||||||
2.4.0
|
2.4.0
|
||||||
|
|
|
@ -3,3 +3,4 @@ include README.rst
|
||||||
include requirements-testing.txt
|
include requirements-testing.txt
|
||||||
include setup.py
|
include setup.py
|
||||||
include LICENSE
|
include LICENSE
|
||||||
|
include pytest_bdd/templates/*.mak
|
||||||
|
|
26
README.rst
26
README.rst
|
@ -731,6 +731,25 @@ To have an output in json format:
|
||||||
py.test --cucumberjson=<path to json report>
|
py.test --cucumberjson=<path to json report>
|
||||||
|
|
||||||
|
|
||||||
|
Test code generation helper
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
For newcomers it's sometimes hard to write all needed test code without being frustrated.
|
||||||
|
To simplify their life, simple code generator was implemented. It allows to create fully functional
|
||||||
|
but of course empty tests and step definitions for given a feature file.
|
||||||
|
It's done as a separate console script provided by pytest-bdd package:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pytest-bdd generate <feature file name> .. <feature file nameN>
|
||||||
|
|
||||||
|
It will print the generated code to the standard output so you can easily redirect it to the file:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
pytest-bdd generate features/some.feature > tests/functional/test_some.py
|
||||||
|
|
||||||
|
|
||||||
Migration of your tests from versions 0.x.x-1.x.x
|
Migration of your tests from versions 0.x.x-1.x.x
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
|
@ -743,15 +762,12 @@ decorator. Reasons for that:
|
||||||
decorator more or not, so to support it along with functional approach there needed to be special parameter
|
decorator more or not, so to support it along with functional approach there needed to be special parameter
|
||||||
for that, which is also a backwards-incompartible change.
|
for that, which is also a backwards-incompartible change.
|
||||||
|
|
||||||
To help users migrate to newer version, there's migration console script provided with **migrate** extra:
|
To help users migrate to newer version, there's migration subcommand of the `pytest-bdd` console script:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
# install extra for migration
|
|
||||||
pip install pytest-bdd[migrate]
|
|
||||||
|
|
||||||
# run migration script
|
# run migration script
|
||||||
pytestbdd_migrate_tests <your test folder>
|
pytest-bdd migrate <your test folder>
|
||||||
|
|
||||||
Under the hood the script does the replacement from this:
|
Under the hood the script does the replacement from this:
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,33 @@
|
||||||
"""pytest-bdd scripts."""
|
"""pytest-bdd scripts."""
|
||||||
|
import argparse
|
||||||
import glob2
|
import itertools
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
import sys
|
|
||||||
|
|
||||||
|
import glob2
|
||||||
|
from mako.lookup import TemplateLookup
|
||||||
|
|
||||||
|
import pytest_bdd
|
||||||
|
from pytest_bdd.feature import Feature
|
||||||
|
|
||||||
|
template_lookup = TemplateLookup(directories=[os.path.join(os.path.dirname(pytest_bdd.__file__), 'templates')])
|
||||||
|
|
||||||
MIGRATE_REGEX = re.compile(r'\s?(\w+)\s\=\sscenario\((.+)\)', flags=re.MULTILINE)
|
MIGRATE_REGEX = re.compile(r'\s?(\w+)\s\=\sscenario\((.+)\)', flags=re.MULTILINE)
|
||||||
|
|
||||||
|
PYTHON_REPLACE_REGEX = re.compile('\W')
|
||||||
|
|
||||||
def migrate_tests():
|
ALPHA_REGEX = re.compile('^\d+_*')
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_tests(args):
|
||||||
"""Migrate outdated tests to the most recent form."""
|
"""Migrate outdated tests to the most recent form."""
|
||||||
if len(sys.argv) != 2:
|
path = args.path
|
||||||
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')):
|
for file_path in glob2.iglob(os.path.join(os.path.abspath(path), '**', '*.py')):
|
||||||
migrate_tests_in_file(file_path)
|
migrate_tests_in_file(file_path)
|
||||||
|
|
||||||
|
@ -33,3 +46,61 @@ def migrate_tests_in_file(file_path):
|
||||||
print('skipped: {0}'.format(file_path))
|
print('skipped: {0}'.format(file_path))
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def check_existense(file_name):
|
||||||
|
"""Check filename for existense."""
|
||||||
|
if not os.path.isfile(file_name):
|
||||||
|
raise argparse.ArgumentTypeError('{0} is an invalid file name'.format(file_name))
|
||||||
|
return file_name
|
||||||
|
|
||||||
|
|
||||||
|
def generate_code(args):
|
||||||
|
"""Generate test code for the given filename."""
|
||||||
|
features = []
|
||||||
|
scenarios = []
|
||||||
|
seen_names = set()
|
||||||
|
for file_name in args.files:
|
||||||
|
if file_name in seen_names:
|
||||||
|
continue
|
||||||
|
seen_names.add(file_name)
|
||||||
|
base, name = os.path.split(file_name)
|
||||||
|
feature = Feature.get_feature(base, name)
|
||||||
|
features.append(feature)
|
||||||
|
scenarios.extend(feature.scenarios.values())
|
||||||
|
|
||||||
|
steps = itertools.chain.from_iterable(
|
||||||
|
scenario.steps for scenario in scenarios)
|
||||||
|
steps = sorted(steps, key=lambda step: step.type)
|
||||||
|
seen_steps = set()
|
||||||
|
grouped_steps = []
|
||||||
|
for step in (itertools.chain.from_iterable(
|
||||||
|
sorted(group, key=lambda step: step.name)
|
||||||
|
for _, group in itertools.groupby(steps, lambda step: step.type))):
|
||||||
|
if step.name not in seen_steps:
|
||||||
|
grouped_steps.append(step)
|
||||||
|
seen_steps.add(step.name)
|
||||||
|
|
||||||
|
print(template_lookup.get_template('test.py.mak').render(
|
||||||
|
feature=features[0], scenarios=scenarios, steps=grouped_steps, make_python_name=make_python_name))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point."""
|
||||||
|
parser = argparse.ArgumentParser(prog='pytest-bdd')
|
||||||
|
subparsers = parser.add_subparsers(help='sub-command help')
|
||||||
|
|
||||||
|
parser_generate = subparsers.add_parser('generate', help='generate help')
|
||||||
|
parser_generate.add_argument(
|
||||||
|
'files', metavar='FEATURE_FILE', type=check_existense, nargs='+',
|
||||||
|
help='Feature files to generate test code with')
|
||||||
|
parser_generate.set_defaults(func=generate_code)
|
||||||
|
|
||||||
|
parser_migrate = subparsers.add_parser('migrate', help='migrate help')
|
||||||
|
parser_migrate.add_argument(
|
||||||
|
'path', metavar='PATH',
|
||||||
|
help='Migrate outdated tests to the most recent form')
|
||||||
|
parser_migrate.set_defaults(func=migrate_tests)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
"""${ feature.name or feature.rel_filename } feature tests."""
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from pytest_bdd import (given, when, then, scenario)
|
||||||
|
|
||||||
|
scenario = partial(scenario, feature.filename)
|
||||||
|
|
||||||
|
|
||||||
|
% for scenario in sorted(scenarios, key=lambda scenario: scenario.name):
|
||||||
|
@scenario('${scenario.name}')
|
||||||
|
def test_${ make_python_name(scenario.name)}():
|
||||||
|
"""${scenario.name}."""
|
||||||
|
|
||||||
|
|
||||||
|
% endfor
|
||||||
|
% for step in steps:
|
||||||
|
@${step.type}('${step.name}')
|
||||||
|
def ${ make_python_name(step.name)}():
|
||||||
|
"""${step.name}."""
|
||||||
|
% if not loop.last:
|
||||||
|
|
||||||
|
|
||||||
|
% endif
|
||||||
|
% endfor
|
8
setup.py
8
setup.py
|
@ -57,6 +57,8 @@ setup(
|
||||||
cmdclass={'test': Tox},
|
cmdclass={'test': Tox},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'pytest>=2.6.0',
|
'pytest>=2.6.0',
|
||||||
|
'glob2',
|
||||||
|
'Mako',
|
||||||
],
|
],
|
||||||
# the following makes a plugin available to py.test
|
# the following makes a plugin available to py.test
|
||||||
entry_points={
|
entry_points={
|
||||||
|
@ -65,12 +67,10 @@ setup(
|
||||||
'pytest-bdd-cucumber-json = pytest_bdd.cucumber_json',
|
'pytest-bdd-cucumber-json = pytest_bdd.cucumber_json',
|
||||||
],
|
],
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'pytestbdd_migrate_tests = pytest_bdd.scripts:migrate_tests [migrate]'
|
'pytest-bdd = pytest_bdd.scripts:main'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
tests_require=['detox'],
|
tests_require=['detox'],
|
||||||
extras_require={
|
|
||||||
'migrate': ['glob2']
|
|
||||||
},
|
|
||||||
packages=['pytest_bdd'],
|
packages=['pytest_bdd'],
|
||||||
|
include_package_data=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
Feature: Code generation
|
||||||
|
|
||||||
|
Scenario: Given and when using the same fixture should not evaluate it twice
|
||||||
|
Given I have an empty list
|
||||||
|
And 1 have a fixture (appends 1 to a list) in reuse syntax
|
||||||
|
|
||||||
|
When I use this fixture
|
||||||
|
|
||||||
|
Then my list should be [1]
|
|
@ -0,0 +1,49 @@
|
||||||
|
"""Test code generation command."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
from pytest_bdd.scripts import main
|
||||||
|
|
||||||
|
PATH = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate(monkeypatch, capsys):
|
||||||
|
"""Test if the code is generated by a given feature."""
|
||||||
|
monkeypatch.setattr(sys, 'argv', ['', 'generate', os.path.join(PATH, 'generate.feature')])
|
||||||
|
main()
|
||||||
|
out, err = capsys.readouterr()
|
||||||
|
assert out == textwrap.dedent('''
|
||||||
|
"""Code generation feature tests."""
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from pytest_bdd import (given, when, then, scenario)
|
||||||
|
|
||||||
|
scenario = partial(scenario, feature.filename)
|
||||||
|
|
||||||
|
|
||||||
|
@scenario('Given and when using the same fixture should not evaluate it twice')
|
||||||
|
def test_Given_and_when_using_the_same_fixture_should_not_evaluate_it_twice():
|
||||||
|
"""Given and when using the same fixture should not evaluate it twice."""
|
||||||
|
|
||||||
|
|
||||||
|
@given('1 have a fixture (appends 1 to a list) in reuse syntax')
|
||||||
|
def have_a_fixture_appends_1_to_a_list_in_reuse_syntax():
|
||||||
|
"""1 have a fixture (appends 1 to a list) in reuse syntax."""
|
||||||
|
|
||||||
|
|
||||||
|
@given('I have an empty list')
|
||||||
|
def I_have_an_empty_list():
|
||||||
|
"""I have an empty list."""
|
||||||
|
|
||||||
|
|
||||||
|
@then('my list should be [1]')
|
||||||
|
def my_list_should_be_1():
|
||||||
|
"""my list should be [1]."""
|
||||||
|
|
||||||
|
|
||||||
|
@when('I use this fixture')
|
||||||
|
def I_use_this_fixture():
|
||||||
|
"""I use this fixture."""
|
||||||
|
|
||||||
|
'''[1:].replace(u"'", u"'"))
|
6
tox.ini
6
tox.ini
|
@ -22,12 +22,6 @@ deps =
|
||||||
hg+https://bitbucket.org/hpk42/py#egg=py
|
hg+https://bitbucket.org/hpk42/py#egg=py
|
||||||
hg+https://bitbucket.org/hpk42/pytest#egg=pytest
|
hg+https://bitbucket.org/hpk42/pytest#egg=pytest
|
||||||
|
|
||||||
[testenv:py27-pytest-2.4.2]
|
|
||||||
basepython=python2.7
|
|
||||||
deps =
|
|
||||||
{[testenv]deps}
|
|
||||||
pytest==2.4.2
|
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
pep8maxlinelength=120
|
pep8maxlinelength=120
|
||||||
addopts=-vvl
|
addopts=-vvl
|
||||||
|
|
Loading…
Reference in New Issue