Implement simple code generation command. Closes #32

This commit is contained in:
Anatoly Bubenkov 2014-09-18 03:30:51 +00:00
parent c21dda176c
commit 57431dc218
10 changed files with 188 additions and 23 deletions

View File

@ -5,6 +5,7 @@ Unreleased
----------
- Better reporting of a not found scenario (bubenkoff)
- Simple test code generation implemented (bubenkoff)
2.4.0

View File

@ -3,3 +3,4 @@ include README.rst
include requirements-testing.txt
include setup.py
include LICENSE
include pytest_bdd/templates/*.mak

View File

@ -731,6 +731,25 @@ To have an output in json format:
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
-------------------------------------------------
@ -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
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
pytestbdd_migrate_tests <your test folder>
pytest-bdd migrate <your test folder>
Under the hood the script does the replacement from this:

View File

@ -1,20 +1,33 @@
"""pytest-bdd scripts."""
import glob2
import argparse
import itertools
import os.path
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)
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."""
if len(sys.argv) != 2:
print('Usage: pytestbdd_migrate_tests <path>')
sys.exit(1)
path = sys.argv[1]
path = args.path
for file_path in glob2.iglob(os.path.join(os.path.abspath(path), '**', '*.py')):
migrate_tests_in_file(file_path)
@ -33,3 +46,61 @@ def migrate_tests_in_file(file_path):
print('skipped: {0}'.format(file_path))
except IOError:
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)

View File

@ -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

View File

@ -57,6 +57,8 @@ setup(
cmdclass={'test': Tox},
install_requires=[
'pytest>=2.6.0',
'glob2',
'Mako',
],
# the following makes a plugin available to py.test
entry_points={
@ -65,12 +67,10 @@ setup(
'pytest-bdd-cucumber-json = pytest_bdd.cucumber_json',
],
'console_scripts': [
'pytestbdd_migrate_tests = pytest_bdd.scripts:migrate_tests [migrate]'
'pytest-bdd = pytest_bdd.scripts:main'
]
},
tests_require=['detox'],
extras_require={
'migrate': ['glob2']
},
packages=['pytest_bdd'],
include_package_data=True,
)

View File

View File

@ -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]

View File

@ -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"'"))

View File

@ -22,12 +22,6 @@ deps =
hg+https://bitbucket.org/hpk42/py#egg=py
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]
pep8maxlinelength=120
addopts=-vvl