From abc4ab0d470069508e03a497976fb4a26d0d0dac Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sat, 17 Aug 2013 17:57:33 +0200 Subject: [PATCH] less strict parametrization checks. preserve docstring for steps. --- .gitignore | 1 + .travis.yml | 12 +-- CHANGELOG => CHANGES.rst | 4 +- MANIFEST.in | 5 +- README.md => README.rst | 114 +++++++++++++++++------------ pytest_bdd/scenario.py | 10 ++- pytest_bdd/steps.py | 27 +++---- requirements-testing.txt | 3 +- setup.py | 100 ++++--------------------- tests/feature/test_parametrized.py | 38 +--------- tests/steps/test_steps.py | 17 ++++- tox.ini | 22 ++++++ 12 files changed, 157 insertions(+), 196 deletions(-) rename CHANGELOG => CHANGES.rst (83%) rename README.md => README.rst (69%) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 1726e89..9187fa7 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ nosetests.xml /lib /include /src +/share diff --git a/.travis.yml b/.travis.yml index d7aecbb..2737e29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,15 @@ language: python -python: - - "2.6" - - "2.7" - - "3.3" +before_install: + - "export DISPLAY=:99.0" + - "sh -e /etc/init.d/xvfb start" # command to install dependencies -install: pip install -e . python-coveralls +install: + - pip install python-coveralls # # command to run tests script: python setup.py test after_success: + - pip install -r requirements-testing.txt -e . + - py.test --cov=pytest_bdd --cov-report=term-missing tests - coveralls notifications: email: diff --git a/CHANGELOG b/CHANGES.rst similarity index 83% rename from CHANGELOG rename to CHANGES.rst index 9966528..8bfa97a 100644 --- a/CHANGELOG +++ b/CHANGES.rst @@ -4,6 +4,7 @@ Changes between 0.4.7 and 0.5.0 - Added parametrization to scenarios - Coveralls.io integration - Test coverage improvement/fixes +- Correct wrapping of step functions to preserve function docstring Changes between 0.4.6 and 0.4.7 @@ -11,6 +12,7 @@ Changes between 0.4.6 and 0.4.7 - Fixed Python 3.3 support + Changes between 0.4.5 and 0.4.6 ------------------------------- @@ -26,4 +28,4 @@ Changes between 0.4.3 and 0.4.5 Changes between 0.4.1 and 0.4.3 ------------------------------- -- Update the license file and PYPI related documentation. \ No newline at end of file +- Update the license file and PYPI related documentation. diff --git a/MANIFEST.in b/MANIFEST.in index ffeb8d9..168f470 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ -include CHANGELOG -include README.md +include CHANGES.rst +include README.rst +include requirements-testing.txt include setup.py include LICENSE diff --git a/README.md b/README.rst similarity index 69% rename from README.md rename to README.rst index dc46e2f..2a5f299 100644 --- a/README.md +++ b/README.rst @@ -1,21 +1,27 @@ BDD library for the py.test runner ================================== -[![Build Status](https://api.travis-ci.org/olegpidsadnyi/pytest-bdd.png)](https://travis-ci.org/olegpidsadnyi/pytest-bdd) -[![Pypi](https://pypip.in/v/pytest-bdd/badge.png)](https://crate.io/packages/pytest-bdd/) -[![Coverrals](https://coveralls.io/repos/olegpidsadnyi/pytest-bdd/badge.png?branch=master)](https://coveralls.io/r/olegpidsadnyi/pytest-bdd) +.. |Build Status| image:: https://api.travis-ci.org/olegpidsadnyi/pytest-bdd.png + :target: https://travis-ci.org/olegpidsadnyi/pytest-bdd +.. |Pypi| image:: https://pypip.in/v/pytest-bdd/badge.png + :target: https://crate.io/packages/pytest-bdd/ +.. |Coverrals| image:: https://coveralls.io/repos/olegpidsadnyi/pytest-bdd/badge.png?branch=master + :target: https://coveralls.io/r/olegpidsadnyi/pytest-bdd Install pytest-bdd -================= +================== + +:: pip install pytest-bdd - Example ======= -publish_article.feature: +publish\_article.feature: + +:: Scenario: Publishing the article Given I'm an author user @@ -25,8 +31,9 @@ publish_article.feature: Then I should not see the error message And the article should be published # Note: will query the database +test\_publish\_article.py: -test_publish_article.py: +:: from pytest_bdd import scenario, given, when, then @@ -59,25 +66,28 @@ test_publish_article.py: article.refresh() # Refresh the object in the SQLAlchemy session assert article.is_published - Step aliases ============ -Sometimes it is needed to declare the same fixtures or steps with the different names -for better readability. -In order to use the same step function with multiple step names simply -decorate it multiple times: +Sometimes it is needed to declare the same fixtures or steps with the +different names for better readability. In order to use the same step +function with multiple step names simply decorate it multiple times: +:: @given('I have an article') @given('there\'s an article') def article(author): return create_test_article(author=author) -Note that the given step aliases are independent and will be executed when mentioned. +Note that the given step aliases are independent and will be executed +when mentioned. -For example if you assoicate your resource to some owner or not. Admin user can't be an -author of the article, but article should have some default author. +For example if you assoicate your resource to some owner or not. Admin +user can’t be an author of the article, but article should have some +default author. + +:: Scenario: I'm the author Given I'm an author @@ -88,26 +98,30 @@ author of the article, but article should have some default author. Given I'm the admin And there is an article - Step parameters =============== -Sometimes it is hard to write good scenarios without duplicating most of contents of existing scenario. -For example if you create some object with static param value, you might want to create another test with -different param value. -By Gherkin specification it's possible to have parameters in steps: http://docs.behat.org/guides/1.gherkin.html +Sometimes it is hard to write good scenarios without duplicating most of +contents of existing scenario. For example if you create some object +with static param value, you might want to create another test with +different param value. By Gherkin specification it’s possible to have +parameters in steps: http://docs.behat.org/guides/1.gherkin.html Example: +:: + Scenario: Parametrized given, when, thens Given there are cucumbers When I eat cucumbers Then I should have cucumbers -As you can see we don't use Scenario Outline, but use just Scenario, just because it's simple to implement for pytest. - +As you can see we don’t use Scenario Outline, but use just Scenario, +just because it’s simple to implement for pytest. The code will look like: +:: + # here we use pytest power to parametrize test @pytest.mark.parametrize( ['start', 'eat', 'left'], @@ -116,7 +130,8 @@ The code will look like: 'parametrized.feature', 'Parametrized given, when, thens', ) - # note that we should receive same arguments in function that we use for test parametrization + # note that we should receive same arguments in function that we use for test parametrization either directly + # or indirectly (throught fixtures) def test_parametrized(start, eat, left): """We don't need to do anything here, everything will be managed by scenario decorator.""" @@ -137,31 +152,34 @@ The code will look like: assert start_cucumbers['start'] == start assert start_cucumbers['eat'] == eat - Reuse fixtures -================ +============== -Sometimes scenarios define new names for the fixture that can be inherited. -Fixtures can be reused with other names using given(): +Sometimes scenarios define new names for the fixture that can be +inherited. Fixtures can be reused with other names using given(): + +:: given('I have beautiful article', fixture='article') - Reuse steps =========== -It is possible to define some common steps in the parent conftest.py and simply -expect them in the child test file. +It is possible to define some common steps in the parent conftest.py and +simply expect them in the child test file. -common_steps.feature: +common\_steps.feature: + +:: Scenario: All steps are declared in the conftest Given I have a bar Then bar should have value "bar" - conftest.py: +:: + from pytest_bdd import given, then @@ -174,22 +192,26 @@ conftest.py: def bar_is_bar(bar): assert bar == 'bar' -test_common.py: +test\_common.py: + +:: test_conftest = scenario('common_steps.feature', 'All steps are declared in the conftest') - -There are no definitions of the steps in the test file. They were collected from the parent -conftests. - +There are no definitions of the steps in the test file. They were +collected from the parent conftests. Feature file paths ================== -But default, pytest-bdd will use current module's path as base path for finding feature files, but this behaviour can -be changed by having fixture named 'pytestbdd_feature_base_dir' which should return the new base path. +But default, pytest-bdd will use current module’s path as base path for +finding feature files, but this behaviour can be changed by having +fixture named ‘pytestbdd\_feature\_base\_dir’ which should return the +new base path. -test_publish_article.py: +test\_publish\_article.py: + +:: import pytest from pytest_bdd import scenario @@ -201,21 +223,21 @@ test_publish_article.py: test_publish = scenario('publish_article.feature', 'Publishing the article') - Subplugins ========== -The pytest BDD has plugin support, and the main purpose of plugins (subplugins) is to provide useful and specialized -fixtures. +The pytest BDD has plugin support, and the main purpose of plugins +(subplugins) is to provide useful and specialized fixtures. List of known subplugins: - * pytest-bdd-splinter -- collection of fixtures for real browser BDD testing +:: + * pytest-bdd-splinter -- collection of fixtures for real browser BDD testing License ======= -This software is licensed under the [MIT license](http://en.wikipedia.org/wiki/MIT_License>). +This software is licensed under the `MIT license `_. -© 2013 Oleg Pidsadnyi \ No newline at end of file +© 2013 Oleg Pidsadnyi diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index b5375e0..2cae1f6 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -45,11 +45,13 @@ def scenario(feature_name, scenario_name): raise ScenarioNotFound( 'Scenario "{0}" in feature "{1}" is not found'.format(scenario_name, feature_name)) - if scenario.params != _scenario.pytestbdd_params: + resolved_params = scenario.params.intersection(request.fixturenames) + + if scenario.params != resolved_params: raise NotEnoughScenarioParams( - """Scenario "{0}" in feature "{1}" doesn't have enough parameters declared. -Should declare params: {2}, but declared only: {3}""".format( - scenario_name, feature_name, sorted(scenario.params), sorted(_scenario.pytestbdd_params))) + """Scenario "{0}" in feature "{1}" was not able to resolve all parameters declared. +Should resolve params: {2}, but resolved only: {3}""".format( + scenario_name, feature_name, sorted(scenario.params), sorted(resolved_params))) # Execute scenario's steps for step in scenario.steps: diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index 8c7d9af..15477c3 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -38,7 +38,7 @@ import sys # pragma: no cover import pytest # pragma: no cover -from pytest_bdd.feature import remove_prefix, get_step_params # pragma: no cover +from pytest_bdd.feature import remove_prefix # pragma: no cover from pytest_bdd.types import GIVEN, WHEN, THEN # pragma: no cover PY3 = sys.version_info[0] >= 3 # pragma: no cover @@ -48,10 +48,6 @@ class StepError(Exception): # pragma: no cover pass -class NotEnoughStepParams(Exception): # pragma: no cover - pass - - def given(name, fixture=None): """Given step decorator. @@ -111,30 +107,31 @@ def _step_decorator(step_type, step_name): """ step_name = remove_prefix(step_name) - step_params = set(get_step_params(step_name)) - def decorator(func): + step_func = func - if step_params: - step_func_args = inspect.getargspec(step_func).args - if step_params.intersection(step_func_args) != step_params: - raise NotEnoughStepParams( - """Step "{0}" doesn't have enough parameters declared. -Should declare params: {1}, but declared only: {2}""".format(step_name, sorted(step_params), sorted(step_func_args))) if step_type == GIVEN: if not hasattr(func, '_pytestfixturefunction'): # avoid overfixturing of a fixture func = pytest.fixture(func) step_func = lambda request: request.getfuncargvalue(func.__name__) + step_func.__doc__ = func.__doc__ step_func.__name__ = step_name + + @pytest.fixture + def lazy_step_func(): + return step_func + + # preserve docstring + lazy_step_func.__doc__ = func.__doc__ + contribute_to_module( get_caller_module(), step_name, - pytest.fixture(lambda: step_func), + lazy_step_func, ) - return func return decorator diff --git a/requirements-testing.txt b/requirements-testing.txt index b87e178..73e249e 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,4 +1,5 @@ mock pytest-pep8 pytest-cov -pytest-cache \ No newline at end of file +pytest-cache +pytest-xdist diff --git a/setup.py b/setup.py index f8158a5..65f2d28 100755 --- a/setup.py +++ b/setup.py @@ -1,78 +1,3 @@ -#!/usr/bin/env python -""" -PyTest-BDD -========== - -Implements a subset of Gherkin language for the behavior-driven development and -automated testing. Benefits from the pytest and its dependency injection pattern -for the true just enough specifications and maximal reusability of the BDD -definitions. - -Example -``````` - -publish_article.feature: - -.. code:: gherkin - - Scenario: Publishing the article - Given I'm an author user - And I have an article - When I go to the article page - And I press the publish button - Then I should not see the error message - And the article should be published # Note: will query the database - - -test_publish_article.py: - -.. code:: python - - from pytest_bdd import scenario, given, when, then - - test_publish = scenario('publish_article.feature', 'Publishing the article') - - - @given('I have an article') - def article(author): - return create_test_article(author=author) - - - @when('I go to the article page') - def go_to_article(article, browser): - browser.visit(urljoin(browser.url, '/manage/articles/{0}/'.format(article.id))) - - - @when('I press the publish button') - def publish_article(browser): - browser.find_by_css('button[name=publish]').first.click() - - - @then('I should not see the error message') - def no_error_message(browser): - with pytest.raises(ElementDoesNotExist): - browser.find_by_css('.message.error').first - - - @then('And the article should be published') - def article_is_published(article): - article.refresh() # Refresh the object in the SQLAlchemy session - assert article.is_published - -Installation -```````````` - -.. code:: bash - - $ pip install pytest-bdd - -Links -````` - -* `website `_ -* `documentation `_ - -""" import os import sys @@ -80,25 +5,30 @@ from setuptools import setup from setuptools.command.test import test as TestCommand -class PyTest(TestCommand): - +class Tox(TestCommand): def finalize_options(self): TestCommand.finalize_options(self) - self.test_args = ['tests', '-pep8', '--cov', 'pytest_bdd', '--cov-report', 'term-missing'] + self.test_args = ['--recreate'] self.test_suite = True def run_tests(self): - # The import is here, cause outside the eggs aren't loaded - import pytest - errno = pytest.main(self.test_args) + #import here, cause outside the eggs aren't loaded + import tox + errno = tox.cmdline(self.test_args) sys.exit(errno) -tests_require = open(os.path.join(os.path.dirname(__file__), 'requirements-testing.txt')).read().split() + +dirname = os.path.dirname(__file__) + +long_description = ( + open(os.path.join(dirname, 'README.rst')).read() + '\n' + + open(os.path.join(dirname, 'CHANGES.rst')).read() +) setup( name='pytest-bdd', description='BDD for pytest', - long_description=__doc__, + long_description=long_description, author='Oleg Pidsadnyi', license='MIT license', author_email='oleg.podsadny@gmail.com', @@ -117,7 +47,7 @@ setup( 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3' ] + [('Programming Language :: Python :: %s' % x) for x in '2.6 2.7 3.0 3.1 3.2 3.3'.split()], - cmdclass={'test': PyTest}, + cmdclass={'test': Tox}, install_requires=[ 'pytest', ], @@ -127,6 +57,6 @@ setup( 'pytest-bdd = pytest_bdd.plugin', ] }, - tests_require=tests_require, + tests_require=['tox'], packages=['pytest_bdd'], ) diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index 281d8c2..d70ef8c 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -1,6 +1,5 @@ import pytest -from pytest_bdd.steps import NotEnoughStepParams from pytest_bdd.scenario import NotEnoughScenarioParams from pytest_bdd import given, when, then, scenario @@ -17,39 +16,6 @@ def test_parametrized(request, start, eat, left): """Test parametrized scenario.""" -def test_parametrized_given(): - """Test parametrized given.""" - with pytest.raises(NotEnoughStepParams) as exc: - @given('there are cucumbers') - def some_cucumbers(): - return {} - assert exc.value.args == ( - 'Step "there are cucumbers" doesn\'t have enough parameters declared.\n' - 'Should declare params: [\'some\'], but declared only: []',) - - -def test_parametrized_when(): - """Test parametrized when.""" - with pytest.raises(NotEnoughStepParams) as exc: - @when('I eat cucumbers') - def some_cucumbers(): - return {} - assert exc.value.args == ( - 'Step "I eat cucumbers" doesn\'t have enough parameters declared.\n' - 'Should declare params: [\'some\'], but declared only: []',) - - -def test_parametrized_then(): - """Test parametrized then.""" - with pytest.raises(NotEnoughStepParams) as exc: - @when('I should have cucumbers') - def some_cucumbers(): - return {} - assert exc.value.args == ( - 'Step "I should have cucumbers" doesn\'t have enough parameters declared.\n' - 'Should declare params: [\'some\'], but declared only: []',) - - def test_parametrized_wrongly(request): """Test parametrized scenario when the test function lacks parameters.""" @scenario( @@ -63,8 +29,8 @@ def test_parametrized_wrongly(request): test_parametrized_wrongly(request) assert exc.value.args == ( - 'Scenario "Parametrized given, when, thens" in feature "parametrized.feature" doesn\'t have enough ' - 'parameters declared.\nShould declare params: [\'eat\', \'left\', \'start\'], but declared only: []', + 'Scenario "Parametrized given, when, thens" in feature "parametrized.feature" was not able to resolve all ' + 'parameters declared.\nShould resolve params: [\'eat\', \'left\', \'start\'], but resolved only: []', ) diff --git a/tests/steps/test_steps.py b/tests/steps/test_steps.py index 88c463b..ee7be48 100644 --- a/tests/steps/test_steps.py +++ b/tests/steps/test_steps.py @@ -1,6 +1,7 @@ """Test when and then steps are callables.""" -from pytest_bdd import when, then +import pytest +from pytest_bdd import given, when, then @when('I do stuff') @@ -24,3 +25,17 @@ def test_when_then(request): check_stuff_ = request.getfuncargvalue('I check stuff') assert callable(check_stuff_) + + +@pytest.mark.parametrize( + ('step', 'keyword'), [ + (given, 'Given'), + (when, 'When'), + (then, 'Then')]) +def test_preserve_decorator(step, keyword): + """Check that we preserve original function attributes after decorating it.""" + @step(keyword) + def func(): + """Doc string.""" + + assert globals()[keyword].__doc__ == 'Doc string.' diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..955f882 --- /dev/null +++ b/tox.ini @@ -0,0 +1,22 @@ +[tox] +distshare={homedir}/.tox/distshare +envlist=py26,py27,py27-xdist,py33 +indexserver= + pypi = https://pypi.python.org/simple + +[testenv] +commands= py.test --pep8 --junitxml={envlogdir}/junit-{envname}.xml +deps = -r{toxinidir}/requirements-testing.txt + +[testenv:py27-coverage] +commands= py.test --cov=pytest_bdd --pep8 --junitxml={envlogdir}/junit-{envname}.xml + +[testenv:py27-xdist] +basepython=python2.7 +commands= + py.test -n3 -rfsxX \ + --junitxml={envlogdir}/junit-{envname}.xml + +[pytest] +addopts=tests +pep8maxlinelength=120