diff --git a/CHANGES.rst b/CHANGES.rst index a55044d..f3f8867 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,7 @@ Unreleased ---------- - Better reporting of a not found scenario (bubenkoff) +- Simple test code generation implemented (bubenkoff) 2.4.0 diff --git a/MANIFEST.in b/MANIFEST.in index 168f470..9df9f17 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,4 @@ include README.rst include requirements-testing.txt include setup.py include LICENSE +include pytest_bdd/templates/*.mak diff --git a/README.rst b/README.rst index 86f968c..8ad2401 100644 --- a/README.rst +++ b/README.rst @@ -731,6 +731,25 @@ To have an output in json format: py.test --cucumberjson= +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 .. + +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 + pytest-bdd migrate Under the hood the script does the replacement from this: diff --git a/pytest_bdd/scripts.py b/pytest_bdd/scripts.py index 05c0388..cd1a22f 100644 --- a/pytest_bdd/scripts.py +++ b/pytest_bdd/scripts.py @@ -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 ') - 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) diff --git a/pytest_bdd/templates/test.py.mak b/pytest_bdd/templates/test.py.mak new file mode 100644 index 0000000..e26a8fb --- /dev/null +++ b/pytest_bdd/templates/test.py.mak @@ -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 diff --git a/setup.py b/setup.py index a19624d..4f64177 100755 --- a/setup.py +++ b/setup.py @@ -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, ) diff --git a/tests/scripts/__init__.py b/tests/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/scripts/generate.feature b/tests/scripts/generate.feature new file mode 100644 index 0000000..e6d6c3e --- /dev/null +++ b/tests/scripts/generate.feature @@ -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] diff --git a/tests/scripts/test_generate.py b/tests/scripts/test_generate.py new file mode 100644 index 0000000..49e3afd --- /dev/null +++ b/tests/scripts/test_generate.py @@ -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"'")) diff --git a/tox.ini b/tox.ini index cf47be8..4de6549 100644 --- a/tox.ini +++ b/tox.ini @@ -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