less strict parametrization checks. preserve docstring for steps.
This commit is contained in:
parent
ec475c66ee
commit
abc4ab0d47
|
@ -45,3 +45,4 @@ nosetests.xml
|
|||
/lib
|
||||
/include
|
||||
/src
|
||||
/share
|
||||
|
|
12
.travis.yml
12
.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:
|
||||
|
|
|
@ -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
|
||||
-------------------------------
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
include CHANGELOG
|
||||
include README.md
|
||||
include CHANGES.rst
|
||||
include README.rst
|
||||
include requirements-testing.txt
|
||||
include setup.py
|
||||
include LICENSE
|
||||
|
|
|
@ -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 <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> 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 <http://en.wikipedia.org/wiki/MIT_License>`_.
|
||||
|
||||
© 2013 Oleg Pidsadnyi
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,3 +2,4 @@ mock
|
|||
pytest-pep8
|
||||
pytest-cov
|
||||
pytest-cache
|
||||
pytest-xdist
|
||||
|
|
100
setup.py
100
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 <https://github.com/olegpidsadnyi/pytest-bdd>`_
|
||||
* `documentation <https://pytest-bdd.readthedocs.org/en/latest/>`_
|
||||
|
||||
"""
|
||||
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'],
|
||||
)
|
||||
|
|
|
@ -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 <some> cucumbers')
|
||||
def some_cucumbers():
|
||||
return {}
|
||||
assert exc.value.args == (
|
||||
'Step "there are <some> 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 <some> cucumbers')
|
||||
def some_cucumbers():
|
||||
return {}
|
||||
assert exc.value.args == (
|
||||
'Step "I eat <some> 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 <some> cucumbers')
|
||||
def some_cucumbers():
|
||||
return {}
|
||||
assert exc.value.args == (
|
||||
'Step "I should have <some> 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: []',
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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.'
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue