forked from test_framework/pytest-bdd
merged with olegpidsadnyi/pytest-bdd
This commit is contained in:
commit
f8f9a06e15
|
@ -45,3 +45,4 @@ nosetests.xml
|
|||
/lib
|
||||
/include
|
||||
/src
|
||||
/share
|
||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -1,12 +1,14 @@
|
|||
language: python
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
# command to install dependencies
|
||||
install: "python setup.py develop"
|
||||
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:
|
||||
- bubenkoff@gmail.com
|
||||
- oleg.podsadny@gmail.com
|
||||
|
|
21
CHANGELOG
21
CHANGELOG
|
@ -1,21 +0,0 @@
|
|||
Changes between 0.4.6 and 0.4.7
|
||||
-------------------------------
|
||||
|
||||
- Fixed Python 3.3 support
|
||||
|
||||
Changes between 0.4.5 and 0.4.6
|
||||
-------------------------------
|
||||
|
||||
- Fixed a bug when py.test --fixtures showed incorrect filenames for the steps.
|
||||
|
||||
|
||||
Changes between 0.4.3 and 0.4.5
|
||||
-------------------------------
|
||||
|
||||
- Fixed a bug with the reuse of the fixture by given steps being evaluated multiple times.
|
||||
|
||||
|
||||
Changes between 0.4.1 and 0.4.3
|
||||
-------------------------------
|
||||
|
||||
- Update the license file and PYPI related documentation.
|
|
@ -0,0 +1,47 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
0.6.0
|
||||
-----
|
||||
|
||||
- Added step arguments support. (curzona, olegpidsadnyi, bubenkoff)
|
||||
- Added checking of the step type order. (markon, olegpidsadnyi)
|
||||
|
||||
|
||||
0.5.2
|
||||
-----
|
||||
|
||||
- Added extra info into output when FeatureError exception raises. (amakhnach)
|
||||
|
||||
|
||||
0.5.0
|
||||
-----
|
||||
|
||||
- Added parametrization to scenarios
|
||||
- Coveralls.io integration
|
||||
- Test coverage improvement/fixes
|
||||
- Correct wrapping of step functions to preserve function docstring
|
||||
|
||||
|
||||
0.4.7
|
||||
-----
|
||||
|
||||
- Fixed Python 3.3 support
|
||||
|
||||
|
||||
0.4.6
|
||||
-----
|
||||
|
||||
- Fixed a bug when py.test --fixtures showed incorrect filenames for the steps.
|
||||
|
||||
|
||||
0.4.5
|
||||
-----
|
||||
|
||||
- Fixed a bug with the reuse of the fixture by given steps being evaluated multiple times.
|
||||
|
||||
|
||||
0.4.3
|
||||
-----
|
||||
|
||||
- Update the license file and PYPI related documentation.
|
|
@ -1,4 +1,5 @@
|
|||
include CHANGELOG
|
||||
include README.md
|
||||
include CHANGES.rst
|
||||
include README.rst
|
||||
include requirements-testing.txt
|
||||
include setup.py
|
||||
include LICENSE
|
||||
|
|
170
README.md
170
README.md
|
@ -1,170 +0,0 @@
|
|||
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)
|
||||
|
||||
|
||||
Install pytest-bdd
|
||||
=================
|
||||
|
||||
pip install pytest-bdd
|
||||
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
publish_article.feature:
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
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
|
||||
And I have an article
|
||||
|
||||
|
||||
Scenario: I'm the admin
|
||||
Given I'm the admin
|
||||
And there is an article
|
||||
|
||||
|
||||
Reuse fixtures
|
||||
================
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@given('I have a bar')
|
||||
def bar():
|
||||
return 'bar'
|
||||
|
||||
|
||||
@then('bar should have value "bar"')
|
||||
def bar_is_bar(bar):
|
||||
assert bar == 'bar'
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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.
|
||||
|
||||
test_publish_article.py:
|
||||
|
||||
import pytest
|
||||
from pytest_bdd import scenario
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pytestbdd_feature_base_dir():
|
||||
return '/home/user/projects/foo.bar/features'
|
||||
|
||||
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.
|
||||
|
||||
List of known subplugins:
|
||||
|
||||
* 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>).
|
||||
|
||||
© 2013 Oleg Pidsadnyi
|
|
@ -0,0 +1,423 @@
|
|||
BDD library for the py.test runner
|
||||
==================================
|
||||
|
||||
.. image:: https://api.travis-ci.org/olegpidsadnyi/pytest-bdd.png
|
||||
:target: https://travis-ci.org/olegpidsadnyi/pytest-bdd
|
||||
.. image:: https://pypip.in/v/pytest-bdd/badge.png
|
||||
:target: https://crate.io/packages/pytest-bdd/
|
||||
.. image:: https://coveralls.io/repos/olegpidsadnyi/pytest-bdd/badge.png?branch=master
|
||||
:target: https://coveralls.io/r/olegpidsadnyi/pytest-bdd
|
||||
|
||||
pytest-bdd implements a subset of Gherkin language for the automation of the project
|
||||
requirements testing and easier behavioral driven development.
|
||||
|
||||
Unlike many other BDD tools it doesn't require a separate runner and benefits from
|
||||
the power and flexibility of the pytest. It allows to unify your unit and functional
|
||||
tests, easier continuous integration server configuration and maximal reuse of the
|
||||
tests setup.
|
||||
|
||||
Pytest fixtures written for the unit tests can be reused for the setup and actions
|
||||
mentioned in the feature steps with dependency injection, which allows a true BDD
|
||||
just-enough specification of the requirements without maintaining any context object
|
||||
containing the side effects of the Gherkin imperative declarations.
|
||||
|
||||
Install pytest-bdd
|
||||
==================
|
||||
|
||||
::
|
||||
|
||||
pip install pytest-bdd
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
publish\_article.feature:
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
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-block:: 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
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@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.
|
||||
|
||||
For example if you associate your resource to some owner or not. Admin
|
||||
user can’t be an author of the article, but articles should have a
|
||||
default author.
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Scenario: I'm the author
|
||||
Given I'm an author
|
||||
And I have an article
|
||||
|
||||
|
||||
Scenario: I'm the admin
|
||||
Given I'm the admin
|
||||
And there is an article
|
||||
|
||||
Step arguments
|
||||
==============
|
||||
|
||||
Often it's possible to reuse steps giving them a parameter(s).
|
||||
This allows to have single implementation and multiple use, so less code.
|
||||
Also opens the possibility to use same step twice in single scenario and with different arguments!
|
||||
Important thing that argumented step names are not just strings but regular expressions.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Scenario: Arguments for given, when, thens
|
||||
Given there are 5 cucumbers
|
||||
|
||||
When I eat 3 cucumbers
|
||||
And I eat 2 cucumbers
|
||||
|
||||
Then I should have 0 cucumbers
|
||||
|
||||
|
||||
The code will look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import re
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
|
||||
test_arguments = scenario('arguments.feature', 'Arguments for given, when, thens')
|
||||
|
||||
@given(re.compile('there are (?P<start>\d+) cucumbers'))
|
||||
def start_cucumbers(start):
|
||||
# note that you always get step arguments as strings, convert them on demand
|
||||
start = int(start)
|
||||
return dict(start=start, eat=0)
|
||||
|
||||
|
||||
@when(re.compile('I eat (?P<eat>\d+) cucumbers'))
|
||||
def eat_cucumbers(start_cucumbers, eat):
|
||||
eat = int(eat)
|
||||
start_cucumbers['eat'] += eat
|
||||
|
||||
|
||||
@then(re.compile('I should have (?P<left>\d+) cucumbers'))
|
||||
def should_have_left_cucumbers(start_cucumbers, start, left):
|
||||
start, left = int(start), int(left)
|
||||
assert start_cucumbers['start'] == start
|
||||
assert start - start_cucumbers['eat'] == left
|
||||
|
||||
Step parameters
|
||||
===============
|
||||
|
||||
Scenarios can be parametrized to cover few cases. In Gherkin the variable
|
||||
templates are written using corner braces as <somevalue>.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Scenario: Parametrized given, when, thens
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
||||
Unlike other tools, pytest-bdd implements the scenario outline not in the
|
||||
feature files, but in the python code using pytest parametrization.
|
||||
The reason for this is that it is very often that some simple pythonic type
|
||||
is needed in the parameters like a datetime or a dictionary, which makes it
|
||||
more difficult to express in the text files and preserve the correct format.
|
||||
|
||||
The code will look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
|
||||
# Here we use pytest to parametrize the test with the parameters table
|
||||
@pytest.mark.parametrize(
|
||||
['start', 'eat', 'left'],
|
||||
[(12, 5, 7)])
|
||||
@scenario(
|
||||
'parametrized.feature',
|
||||
'Parametrized given, when, thens',
|
||||
)
|
||||
# Note that we should take the same arguments in the test function that we use
|
||||
# for the test parametrization either directly or indirectly (fixtures depend on them).
|
||||
def test_parametrized(start, eat, left):
|
||||
"""We don't need to do anything here, everything will be managed by the scenario decorator."""
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
def start_cucumbers(start):
|
||||
return dict(start=start)
|
||||
|
||||
|
||||
@when('I eat <eat> cucumbers')
|
||||
def eat_cucumbers(start_cucumbers, start, eat):
|
||||
start_cucumbers['eat'] = eat
|
||||
|
||||
|
||||
@then('I should have <left> cucumbers')
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
assert start - eat == left
|
||||
assert start_cucumbers['start'] == start
|
||||
assert start_cucumbers['eat'] == eat
|
||||
|
||||
Test setup
|
||||
==========
|
||||
|
||||
Test setup is implemented within the Given section. Even though these steps
|
||||
are executed imperatively to apply possible side-effects, pytest-bdd is trying
|
||||
to benefit of the PyTest fixtures which is based on the dependency injection
|
||||
and makes the setup more declarative style.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@given('I have a beautiful article')
|
||||
def article():
|
||||
return Article(is_beautiful=True)
|
||||
|
||||
This also declares a PyTest fixture "article" and any other step can depend on it.
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Given I have a beautiful article
|
||||
When I publish this article
|
||||
|
||||
When step is referring the article to publish it.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@when('I publish this article')
|
||||
def publish_article(article):
|
||||
article.publish()
|
||||
|
||||
Many other BDD toolkits operate a global context and put the side effects there.
|
||||
This makes it very difficult to implement the steps, because the dependencies
|
||||
appear only as the side-effects in the run-time and not declared in the code.
|
||||
The publish article step has to trust that the article is already in the context,
|
||||
has to know the name of the attribute it is stored there, the type etc.
|
||||
|
||||
In pytest-bdd you just declare an argument of the step function that it depends on
|
||||
and the PyTest will make sure to provide it.
|
||||
|
||||
Still side effects can be applied in the imperative style by design of the BDD.
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Given I have a beautiful article
|
||||
And my article is published
|
||||
|
||||
Functional tests can reuse your fixture libraries created for the unit-tests and upgrade
|
||||
them by applying the side effects.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
given('I have a beautiful article', fixture='article')
|
||||
|
||||
@given('my article is published')
|
||||
def published_article(article):
|
||||
article.publish()
|
||||
return article
|
||||
|
||||
This way side-effects were applied to our article and PyTest makes sure that all
|
||||
steps that require the "article" fixture will receive the same object. The value
|
||||
of the "published_article" and the "article" fixtures is the same object.
|
||||
|
||||
Fixtures are evaluated only once within the PyTest scope and their values are cached.
|
||||
In case of Given steps and the step arguments mentioning the same given step makes
|
||||
no sense. It won't be executed second time.
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Given I have a beautiful article
|
||||
And some other thing
|
||||
And I have a beautiful article # Won't be executed, exception is raised
|
||||
|
||||
|
||||
pytest-bdd will raise an exception even in the case of the steps that use regular expression
|
||||
patterns to get arguments.
|
||||
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Given I have 1 cucumbers
|
||||
And I have 2 cucumbers # Exception is raised
|
||||
|
||||
Will raise an exception if the step is using the regular expression pattern.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@given(re.compile('I have (?P<n>\d+) cucumbers'))
|
||||
def cucumbers(n):
|
||||
return create_cucumbers(n)
|
||||
|
||||
|
||||
Reusing fixtures
|
||||
================
|
||||
|
||||
Sometimes scenarios define new names for the fixture that can be
|
||||
inherited. Fixtures can be reused with other names using given():
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
given('I have beautiful article', fixture='article')
|
||||
|
||||
Reusing steps
|
||||
=============
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: feature
|
||||
|
||||
Scenario: All steps are declared in the conftest
|
||||
Given I have a bar
|
||||
Then bar should have value "bar"
|
||||
|
||||
conftest.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pytest_bdd import given, then
|
||||
|
||||
|
||||
@given('I have a bar')
|
||||
def bar():
|
||||
return 'bar'
|
||||
|
||||
|
||||
@then('bar should have value "bar"')
|
||||
def bar_is_bar(bar):
|
||||
assert bar == 'bar'
|
||||
|
||||
test\_common.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
test\_publish\_article.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
from pytest_bdd import scenario
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pytestbdd_feature_base_dir():
|
||||
return '/home/user/projects/foo.bar/features'
|
||||
|
||||
test_publish = scenario('publish_article.feature', 'Publishing the article')
|
||||
|
||||
|
||||
Avoid retyping the feature file name
|
||||
====================================
|
||||
|
||||
If you want to avoid retyping the feature file name when defining your scenarios in a test file, use functools.partial.
|
||||
This will make your life much easier when defining multiple scenarios in a test file.
|
||||
|
||||
For example:
|
||||
|
||||
|
||||
test\_publish\_article.py:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from functools import partial
|
||||
|
||||
import pytest_bdd
|
||||
|
||||
|
||||
scenario = partial(pytest_bdd.scenario, '/path/to/publish_article.feature')
|
||||
|
||||
test_publish = scenario('Publishing the article')
|
||||
test_publish_unprivileged = scenario('Publishing the article as unprivileged user')
|
||||
|
||||
|
||||
You can learn more about `functools.partial <http://docs.python.org/2/library/functools.html#functools.partial>`_ in the Python docs.
|
||||
|
||||
Subplugins
|
||||
==========
|
||||
|
||||
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 the real browser BDD testing
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_.
|
||||
|
||||
© 2013 Oleg Pidsadnyi
|
14
docs/conf.py
14
docs/conf.py
|
@ -11,7 +11,7 @@
|
|||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
import pytest_bdd
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
|
@ -41,16 +41,16 @@ master_doc = 'index'
|
|||
|
||||
# General information about the project.
|
||||
project = u'Pytest-BDD'
|
||||
copyright = u'2013, Oleg Pidsadnyi, Anatoly Bubenkov'
|
||||
copyright = u'2013, Oleg Pidsadnyi'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
version = pytest_bdd.__version__
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1'
|
||||
release = pytest_bdd.__version__
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -184,7 +184,7 @@ latex_elements = {
|
|||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'Pytest-BDD.tex', u'Pytest-BDD Documentation',
|
||||
u'Oleg Pidsadnyi, Anatoly Bubenkov', 'manual'),
|
||||
u'Oleg Pidsadnyi', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
|
@ -214,7 +214,7 @@ latex_documents = [
|
|||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'pytest-bdd', u'Pytest-BDD Documentation',
|
||||
[u'Oleg Pidsadnyi, Anatoly Bubenkov'], 1)
|
||||
[u'Oleg Pidsadnyi'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
|
@ -228,7 +228,7 @@ man_pages = [
|
|||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'Pytest-BDD', u'Pytest-BDD Documentation',
|
||||
u'Oleg Pidsadnyi, Anatoly Bubenkov', 'Pytest-BDD', 'One line description of project.',
|
||||
u'Oleg Pidsadnyi', 'Pytest-BDD', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
|
|
@ -1,24 +1,6 @@
|
|||
.. Pytest-BDD documentation master file, created by
|
||||
sphinx-quickstart on Sun Apr 7 21:07:56 2013.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to Pytest-BDD's documentation!
|
||||
======================================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
.. automodule:: pytest_bdd
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
.. contents::
|
||||
|
||||
.. include:: ../README.rst
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from pytest_bdd.steps import given, when, then
|
||||
from pytest_bdd.scenario import scenario
|
||||
from pytest_bdd.steps import given, when, then # pragma: no cover
|
||||
from pytest_bdd.scenario import scenario # pragma: no cover
|
||||
|
||||
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__]
|
||||
|
||||
__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__] # pragma: no cover
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
"""Feature.
|
||||
|
||||
The way of describing the behavior is based on Gherkin language, but a very
|
||||
limited version. It doesn't support any parameter tables or any variables.
|
||||
limited version. It doesn't support any parameter tables.
|
||||
If the parametrization is needed to generate more test cases it can be done
|
||||
on the fixture level of the pytest.
|
||||
The <variable> syntax can be used here to make a connection between steps and
|
||||
it will also validate the parameters mentioned in the steps with ones
|
||||
provided in the pytest parametrization table.
|
||||
|
||||
Syntax example:
|
||||
|
||||
|
@ -18,21 +21,30 @@ Syntax example:
|
|||
:note: The "#" symbol is used for comments.
|
||||
:note: There're no multiline steps, the description of the step must fit in
|
||||
one line.
|
||||
|
||||
"""
|
||||
import re # pragma: no cover
|
||||
|
||||
from pytest_bdd.types import SCENARIO, GIVEN, WHEN, THEN
|
||||
from pytest_bdd.types import SCENARIO, GIVEN, WHEN, THEN # pragma: no cover
|
||||
|
||||
|
||||
class FeatureError(Exception):
|
||||
class FeatureError(Exception): # pragma: no cover
|
||||
"""Feature parse error."""
|
||||
pass
|
||||
|
||||
message = u'{0}.\nLine number: {1}.\nLine: {2}.'
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self).encode('utf-8')
|
||||
|
||||
def __unicode__(self):
|
||||
return self.message.format(*self.args)
|
||||
|
||||
|
||||
# Global features dictionary
|
||||
features = {}
|
||||
features = {} # pragma: no cover
|
||||
|
||||
|
||||
STEP_PREFIXES = {
|
||||
STEP_PREFIXES = { # pragma: no cover
|
||||
'Scenario: ': SCENARIO,
|
||||
'Given ': GIVEN,
|
||||
'When ': WHEN,
|
||||
|
@ -40,7 +52,9 @@ STEP_PREFIXES = {
|
|||
'And ': None, # Unknown step type
|
||||
}
|
||||
|
||||
COMMENT_SYMBOLS = '#'
|
||||
COMMENT_SYMBOLS = '#' # pragma: no cover
|
||||
|
||||
STEP_PARAM_RE = re.compile('\<(.+?)\>') # pragma: no cover
|
||||
|
||||
|
||||
def get_step_type(line):
|
||||
|
@ -54,6 +68,14 @@ def get_step_type(line):
|
|||
return STEP_PREFIXES[prefix]
|
||||
|
||||
|
||||
def get_step_params(name):
|
||||
"""Return step parameters."""
|
||||
params = STEP_PARAM_RE.search(name)
|
||||
if params:
|
||||
return params.groups()
|
||||
return ()
|
||||
|
||||
|
||||
def strip(line):
|
||||
"""Remove leading and trailing whitespaces and comments.
|
||||
|
||||
|
@ -71,7 +93,9 @@ def remove_prefix(line):
|
|||
"""Remove the step prefix (Scenario, Given, When, Then or And).
|
||||
|
||||
:param line: Line of the Feature file.
|
||||
|
||||
:return: Line without the prefix.
|
||||
|
||||
"""
|
||||
for prefix in STEP_PREFIXES:
|
||||
if line.startswith(prefix):
|
||||
|
@ -86,6 +110,7 @@ class Feature(object):
|
|||
"""Parse the feature file.
|
||||
|
||||
:param filename: Relative path to the feature file.
|
||||
|
||||
"""
|
||||
self.scenarios = {}
|
||||
|
||||
|
@ -95,7 +120,7 @@ class Feature(object):
|
|||
|
||||
with open(filename, 'r') as f:
|
||||
content = f.read()
|
||||
for number_of_line, line in enumerate(content.split('\n')):
|
||||
for line_number, line in enumerate(content.split('\n')):
|
||||
line = strip(line)
|
||||
if not line:
|
||||
continue
|
||||
|
@ -104,15 +129,15 @@ class Feature(object):
|
|||
|
||||
if mode == GIVEN and prev_mode not in (GIVEN, SCENARIO):
|
||||
raise FeatureError('Given steps must be the first in withing the Scenario',
|
||||
number_of_line, line, prev_mode, mode)
|
||||
line_number, line)
|
||||
|
||||
if mode == WHEN and prev_mode not in (SCENARIO, GIVEN, WHEN):
|
||||
raise FeatureError('When steps must be the first or follow Given steps',
|
||||
number_of_line, line, prev_mode, mode)
|
||||
line_number, line)
|
||||
|
||||
if mode == THEN and prev_mode not in (GIVEN, WHEN, THEN):
|
||||
raise FeatureError('Then steps must follow Given or When steps',
|
||||
number_of_line, line, prev_mode, mode)
|
||||
line_number, line)
|
||||
|
||||
prev_mode = mode
|
||||
|
||||
|
@ -122,10 +147,21 @@ class Feature(object):
|
|||
if mode == SCENARIO:
|
||||
self.scenarios[line] = scenario = Scenario(line)
|
||||
else:
|
||||
scenario.add_step(line)
|
||||
scenario.add_step(step_name=line, step_type=mode)
|
||||
|
||||
@classmethod
|
||||
def get_feature(cls, filename):
|
||||
"""Get a feature by the filename.
|
||||
|
||||
:param filename: Filename of the feature file.
|
||||
|
||||
:return: `Feature` instance from the parsed feature cache.
|
||||
|
||||
:note: The features are parsed on the execution of the test and
|
||||
stored in the global variable cache to improve the performance
|
||||
when multiple scenarios are referencing the same file.
|
||||
|
||||
"""
|
||||
feature = features.get(filename)
|
||||
if not feature:
|
||||
feature = Feature(filename)
|
||||
|
@ -138,8 +174,23 @@ class Scenario(object):
|
|||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.params = set()
|
||||
self.steps = []
|
||||
|
||||
def add_step(self, step):
|
||||
"""Add step."""
|
||||
self.steps.append(step)
|
||||
def add_step(self, step_name, step_type):
|
||||
"""Add step to the scenario.
|
||||
|
||||
:param step_name: Step name.
|
||||
:param step_type: Step type.
|
||||
|
||||
"""
|
||||
self.params.update(get_step_params(step_name))
|
||||
self.steps.append(Step(name=step_name, type=step_type))
|
||||
|
||||
|
||||
class Step(object):
|
||||
"""Step."""
|
||||
|
||||
def __init__(self, name, type):
|
||||
self.name = name
|
||||
self.type = type
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
"""Pytest plugin entry point. Used for any fixtures needed."""
|
||||
|
||||
import os.path
|
||||
import os.path # pragma: no cover
|
||||
|
||||
import pytest
|
||||
import pytest # pragma: no cover
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture # pragma: no cover
|
||||
def pytestbdd_feature_base_dir(request):
|
||||
return os.path.dirname(request.module.__file__)
|
||||
|
|
|
@ -9,36 +9,138 @@ test_publish_article = scenario(
|
|||
feature_name='publish_article.feature',
|
||||
scenario_name='Publishing the article',
|
||||
)
|
||||
|
||||
"""
|
||||
import inspect
|
||||
from os import path as op
|
||||
import inspect # pragma: no cover
|
||||
from os import path as op # pragma: no cover
|
||||
|
||||
from pytest_bdd.feature import Feature
|
||||
from _pytest import python
|
||||
|
||||
from pytest_bdd.feature import Feature # pragma: no cover
|
||||
from pytest_bdd.steps import recreate_function
|
||||
from pytest_bdd.types import GIVEN
|
||||
|
||||
|
||||
class ScenarioNotFound(Exception):
|
||||
class ScenarioNotFound(Exception): # pragma: no cover
|
||||
"""Scenario Not Found"""
|
||||
|
||||
|
||||
class NotEnoughScenarioParams(Exception): # pragma: no cover
|
||||
"""Scenario function doesn't take enough parameters in the arguments."""
|
||||
|
||||
|
||||
class StepTypeError(Exception): # pragma: no cover
|
||||
"""Step definition is not of the type expected in the scenario."""
|
||||
|
||||
|
||||
class GivenAlreadyUsed(Exception): # pragma: no cover
|
||||
"""Fixture that implements the Given has been already used."""
|
||||
|
||||
|
||||
def _find_step_function(request, name):
|
||||
"""Match the step defined by the regular expression pattern.
|
||||
|
||||
:param request: PyTest request object.
|
||||
:param name: Step name.
|
||||
|
||||
:return: Step function.
|
||||
|
||||
"""
|
||||
try:
|
||||
return request.getfuncargvalue(name)
|
||||
except python.FixtureLookupError:
|
||||
|
||||
for fixturename, fixturedefs in request._fixturemanager._arg2fixturedefs.items():
|
||||
fixturedef = fixturedefs[0]
|
||||
|
||||
pattern = getattr(fixturedef.func, 'pattern', None)
|
||||
match = pattern.match(name) if pattern else None
|
||||
|
||||
if match:
|
||||
for arg, value in match.groupdict().items():
|
||||
fd = python.FixtureDef(
|
||||
request._fixturemanager,
|
||||
fixturedef.baseid,
|
||||
arg,
|
||||
lambda: value, fixturedef.scope, fixturedef.params,
|
||||
fixturedef.unittest,
|
||||
)
|
||||
request._fixturemanager._arg2fixturedefs[arg] = [fd]
|
||||
if arg in request._funcargs:
|
||||
request._funcargs[arg] = value
|
||||
return request.getfuncargvalue(pattern.pattern)
|
||||
raise
|
||||
|
||||
|
||||
def scenario(feature_name, scenario_name):
|
||||
"""Scenario."""
|
||||
"""Scenario. May be called both as decorator and as just normal function."""
|
||||
|
||||
def _scenario(request):
|
||||
# Get the feature
|
||||
base_path = request.getfuncargvalue('pytestbdd_feature_base_dir')
|
||||
feature_path = op.abspath(op.join(base_path, feature_name))
|
||||
feature = Feature.get_feature(feature_path)
|
||||
def decorator(request):
|
||||
|
||||
# Get the scenario
|
||||
try:
|
||||
scenario = feature.scenarios[scenario_name]
|
||||
except KeyError:
|
||||
raise ScenarioNotFound('Scenario "{0}" in feature "{1}" is not found'.format(scenario_name, feature_name))
|
||||
def _scenario(request):
|
||||
# Get the feature
|
||||
base_path = request.getfuncargvalue('pytestbdd_feature_base_dir')
|
||||
feature_path = op.abspath(op.join(base_path, feature_name))
|
||||
feature = Feature.get_feature(feature_path)
|
||||
|
||||
# Execute scenario's steps
|
||||
for step in scenario.steps:
|
||||
func = request.getfuncargvalue(step)
|
||||
kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(func).args)
|
||||
func(**kwargs)
|
||||
# Get the scenario
|
||||
try:
|
||||
scenario = feature.scenarios[scenario_name]
|
||||
except KeyError:
|
||||
raise ScenarioNotFound(
|
||||
'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name)
|
||||
)
|
||||
|
||||
return _scenario
|
||||
resolved_params = scenario.params.intersection(request.fixturenames)
|
||||
|
||||
if scenario.params != resolved_params:
|
||||
raise NotEnoughScenarioParams(
|
||||
"""Scenario "{0}" in the feature "{1}" was not able to resolve all declared parameters."""
|
||||
"""Should resolve params: {2}, but resolved only: {3}.""".format(
|
||||
scenario_name, feature_name, sorted(scenario.params), sorted(resolved_params),
|
||||
)
|
||||
)
|
||||
|
||||
givens = set()
|
||||
# Execute scenario steps
|
||||
for step in scenario.steps:
|
||||
step_func = _find_step_function(request, step.name)
|
||||
|
||||
# Check the step types are called in the correct order
|
||||
if step_func.step_type != step.type:
|
||||
raise StepTypeError(
|
||||
'Wrong step type "{0}" while "{1}" is expected.'.format(step_func.step_type, step.type)
|
||||
)
|
||||
|
||||
# Check if the fixture that implements given step has not been yet used by another given step
|
||||
if step.type == GIVEN:
|
||||
if step_func.fixture in givens:
|
||||
raise GivenAlreadyUsed(
|
||||
'Fixture "{0}" that implements this "{1}" given step has been already used.'.format(
|
||||
step_func.fixture, step.name,
|
||||
)
|
||||
)
|
||||
givens.add(step_func.fixture)
|
||||
|
||||
# Get the step argument values
|
||||
kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(step_func).args)
|
||||
|
||||
# Execute the step
|
||||
step_func(**kwargs)
|
||||
|
||||
_scenario.pytestbdd_params = set()
|
||||
|
||||
if isinstance(request, python.FixtureRequest):
|
||||
# Called as a normal function.
|
||||
return _scenario(request)
|
||||
|
||||
# Used as a decorator. Modify the returned function to add parameters from a decorated function.
|
||||
func_args = inspect.getargspec(request).args
|
||||
if 'request' in func_args:
|
||||
func_args.remove('request')
|
||||
_scenario = recreate_function(_scenario, name=request.__name__, add_args=func_args)
|
||||
_scenario.pytestbdd_params = set(func_args)
|
||||
|
||||
return _scenario
|
||||
|
||||
return decorator
|
||||
|
|
|
@ -31,21 +31,24 @@ Reusing existing fixtures for a different step name:
|
|||
given('I have a beautiful article', fixture='article')
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from types import CodeType
|
||||
import inspect
|
||||
import sys
|
||||
from __future__ import absolute_import # pragma: no cover
|
||||
import re
|
||||
from types import CodeType # pragma: no cover
|
||||
import inspect # pragma: no cover # pragma: no cover
|
||||
import sys # pragma: no cover
|
||||
|
||||
import pytest
|
||||
import pytest # pragma: no cover
|
||||
|
||||
from pytest_bdd.feature import remove_prefix
|
||||
from pytest_bdd.types import GIVEN, WHEN, THEN
|
||||
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
|
||||
PY3 = sys.version_info[0] >= 3 # pragma: no cover
|
||||
|
||||
|
||||
class StepError(Exception):
|
||||
pass
|
||||
class StepError(Exception): # pragma: no cover
|
||||
"""Step declaration error."""
|
||||
|
||||
RE_TYPE = type(re.compile('')) # pragma: no cover
|
||||
|
||||
|
||||
def given(name, fixture=None):
|
||||
|
@ -56,12 +59,18 @@ def given(name, fixture=None):
|
|||
|
||||
:raises: StepError in case of wrong configuration.
|
||||
:note: Can't be used as a decorator when the fixture is specified.
|
||||
|
||||
"""
|
||||
name = remove_prefix(name)
|
||||
|
||||
if fixture is not None:
|
||||
module = get_caller_module()
|
||||
func = lambda: lambda request: request.getfuncargvalue(fixture)
|
||||
contribute_to_module(module, name, pytest.fixture(func))
|
||||
step_func = lambda request: request.getfuncargvalue(fixture)
|
||||
step_func.step_type = GIVEN
|
||||
step_func.__name__ = name
|
||||
step_func.fixture = fixture
|
||||
func = pytest.fixture(lambda: step_func)
|
||||
func.__doc__ = 'Alias for the "{0}" fixture.'.format(fixture)
|
||||
contribute_to_module(module, remove_prefix(name), func)
|
||||
return _not_a_fixture_decorator
|
||||
|
||||
return _step_decorator(GIVEN, name)
|
||||
|
@ -71,7 +80,9 @@ def when(name):
|
|||
"""When step decorator.
|
||||
|
||||
:param name: Step name.
|
||||
|
||||
:raises: StepError in case of wrong configuration.
|
||||
|
||||
"""
|
||||
return _step_decorator(WHEN, name)
|
||||
|
||||
|
@ -80,7 +91,9 @@ def then(name):
|
|||
"""Then step decorator.
|
||||
|
||||
:param name: Step name.
|
||||
|
||||
:raises: StepError in case of wrong configuration.
|
||||
|
||||
"""
|
||||
return _step_decorator(THEN, name)
|
||||
|
||||
|
@ -89,48 +102,75 @@ def _not_a_fixture_decorator(func):
|
|||
"""Function that prevents the decoration.
|
||||
|
||||
:param func: Function that is going to be decorated.
|
||||
|
||||
:raises: `StepError` if was used as a decorator.
|
||||
|
||||
"""
|
||||
raise StepError('Cannot be used as a decorator when the fixture is specified')
|
||||
|
||||
|
||||
def _step_decorator(step_type, step_name):
|
||||
"""Step decorator for the type and the name.
|
||||
|
||||
:param step_type: Step type (GIVEN, WHEN or THEN).
|
||||
:param step_name: Step name as in the feature file.
|
||||
|
||||
:return: Decorator function for the step.
|
||||
|
||||
:raise: StepError if the function doesn't take group names as parameters.
|
||||
|
||||
:note: If the step type is GIVEN it will automatically apply the pytest
|
||||
fixture decorator to the step function.
|
||||
|
||||
"""
|
||||
step_name = remove_prefix(step_name)
|
||||
pattern = None
|
||||
if isinstance(step_name, RE_TYPE):
|
||||
pattern = step_name
|
||||
step_name = pattern.pattern
|
||||
|
||||
def decorator(func):
|
||||
step_func = func
|
||||
|
||||
if step_type == GIVEN:
|
||||
if not hasattr(func, '_pytestfixturefunction'):
|
||||
# avoid overfixturing of a fixture
|
||||
# Avoid multiple wrapping of a fixture
|
||||
func = pytest.fixture(func)
|
||||
step_func = lambda request: request.getfuncargvalue(func.__name__)
|
||||
step_func.__doc__ = func.__doc__
|
||||
step_func.fixture = func.__name__
|
||||
|
||||
step_func.__name__ = step_name
|
||||
step_func.step_type = step_type
|
||||
|
||||
@pytest.fixture
|
||||
def lazy_step_func():
|
||||
return step_func
|
||||
|
||||
# Preserve the docstring
|
||||
lazy_step_func.__doc__ = func.__doc__
|
||||
|
||||
if pattern:
|
||||
lazy_step_func.pattern = pattern
|
||||
|
||||
contribute_to_module(
|
||||
get_caller_module(),
|
||||
step_name,
|
||||
pytest.fixture(lambda: step_func),
|
||||
lazy_step_func,
|
||||
)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def contribute_to_module(module, name, func):
|
||||
"""Contribute a function to a module.
|
||||
def recreate_function(func, module=None, name=None, add_args=()):
|
||||
"""Recreate a function, replacing some info.
|
||||
|
||||
:param module: Module to contribute to.
|
||||
:param name: Attribute name.
|
||||
:param func: Function object.
|
||||
:param module: Module to contribute to.
|
||||
:param add_args: Additional arguments to add to function.
|
||||
|
||||
:return: Function copy.
|
||||
|
||||
"""
|
||||
|
||||
def get_code(func):
|
||||
|
@ -152,12 +192,34 @@ def contribute_to_module(module, name, func):
|
|||
args = []
|
||||
code = get_code(func)
|
||||
for arg in argnames:
|
||||
if arg == 'co_filename':
|
||||
if module is not None and arg == 'co_filename':
|
||||
args.append(module.__file__)
|
||||
elif name is not None and arg == 'co_name':
|
||||
args.append(name)
|
||||
elif arg == 'co_argcount':
|
||||
args.append(getattr(code, arg) + len(add_args))
|
||||
elif arg == 'co_varnames':
|
||||
co_varnames = getattr(code, arg)
|
||||
args.append(co_varnames[:code.co_argcount] + tuple(add_args) + co_varnames[code.co_argcount:])
|
||||
else:
|
||||
args.append(getattr(code, arg))
|
||||
|
||||
set_code(func, CodeType(*args))
|
||||
if name is not None:
|
||||
func.__name__ = name
|
||||
return func
|
||||
|
||||
|
||||
def contribute_to_module(module, name, func):
|
||||
"""Contribute a function to a module.
|
||||
|
||||
:param module: Module to contribute to.
|
||||
:param name: Attribute name.
|
||||
:param func: Function object.
|
||||
|
||||
"""
|
||||
func = recreate_function(func, module=module)
|
||||
|
||||
setattr(module, name, func)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Common type definitions."""
|
||||
|
||||
SCENARIO = 'scenario'
|
||||
GIVEN = 'given'
|
||||
WHEN = 'when'
|
||||
THEN = 'then'
|
||||
SCENARIO = 'scenario' # pragma: no cover
|
||||
GIVEN = 'given' # pragma: no cover
|
||||
WHEN = 'when' # pragma: no cover
|
||||
THEN = 'then' # pragma: no cover
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
mock
|
||||
pytest-pep8
|
||||
pytest-cov
|
||||
pytest-cache
|
||||
pytest-xdist
|
100
setup.py
100
setup.py
|
@ -1,107 +1,43 @@
|
|||
#!/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
|
||||
|
||||
from setuptools import setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
class PyTest(TestCommand):
|
||||
version = '0.6.0'
|
||||
|
||||
|
||||
class Tox(TestCommand):
|
||||
def finalize_options(self):
|
||||
TestCommand.finalize_options(self)
|
||||
self.test_args = []
|
||||
self.test_args = ['--recreate']
|
||||
self.test_suite = True
|
||||
|
||||
def run_tests(self):
|
||||
#import here, cause outside the eggs aren't loaded
|
||||
import pytest
|
||||
errno = pytest.main(self.test_args)
|
||||
import tox
|
||||
errno = tox.cmdline(self.test_args)
|
||||
sys.exit(errno)
|
||||
|
||||
|
||||
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',
|
||||
url='https://github.com/olegpidsadnyi/pytest-bdd',
|
||||
version='0.4.7',
|
||||
version=version,
|
||||
classifiers=[
|
||||
'Development Status :: 6 - Mature',
|
||||
'Intended Audience :: Developers',
|
||||
|
@ -115,7 +51,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',
|
||||
],
|
||||
|
@ -125,6 +61,6 @@ setup(
|
|||
'pytest-bdd = pytest_bdd.plugin',
|
||||
]
|
||||
},
|
||||
tests_require=['mock'],
|
||||
tests_require=['tox'],
|
||||
packages=['pytest_bdd'],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
Scenario: Every step takes a parameter with the same name
|
||||
Given I have 1 Euro
|
||||
When I pay 2 Euro
|
||||
And I pay 1 Euro
|
||||
Then I should have 0 Euro
|
||||
And I should have 999999 Euro # In my dream...
|
||||
|
||||
|
||||
Scenario: Using the same given fixture raises an error
|
||||
Given I have 1 Euro
|
||||
And I have 2 Euro
|
|
@ -0,0 +1,7 @@
|
|||
import re
|
||||
from pytest_bdd import when
|
||||
|
||||
|
||||
@when(re.compile(r'I append (?P<n>\d+) to the list'))
|
||||
def append_to_list(results, n):
|
||||
results.append(int(n))
|
|
@ -0,0 +1,8 @@
|
|||
Scenario: Executed with steps matching step definitons with arguments
|
||||
Given I have a foo fixture with value "foo"
|
||||
And there is a list
|
||||
When I append 1 to the list
|
||||
And I append 2 to the list
|
||||
And I append 3 to the list
|
||||
Then foo should have value "foo"
|
||||
And the list should be [1, 2, 3]
|
|
@ -0,0 +1,27 @@
|
|||
from pytest_bdd import scenario, given, then
|
||||
|
||||
|
||||
test_steps = scenario(
|
||||
'args.feature',
|
||||
'Executed with steps matching step definitons with arguments',
|
||||
)
|
||||
|
||||
|
||||
@given('I have a foo fixture with value "foo"')
|
||||
def foo():
|
||||
return 'foo'
|
||||
|
||||
|
||||
@given('there is a list')
|
||||
def results():
|
||||
return []
|
||||
|
||||
|
||||
@then('foo should have value "foo"')
|
||||
def foo_is_foo(foo):
|
||||
assert foo == 'foo'
|
||||
|
||||
|
||||
@then('the list should be [1, 2, 3]')
|
||||
def check_results(results):
|
||||
assert results == [1, 2, 3]
|
|
@ -0,0 +1,40 @@
|
|||
import re
|
||||
import pytest
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.scenario import GivenAlreadyUsed
|
||||
|
||||
|
||||
test_steps = scenario(
|
||||
'args_steps.feature',
|
||||
'Every step takes a parameter with the same name',
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def values():
|
||||
return ['1', '2', '1', '0', '999999']
|
||||
|
||||
|
||||
@given(re.compile(r'I have (?P<euro>\d+) Euro'))
|
||||
def i_have(euro, values):
|
||||
assert euro == values.pop(0)
|
||||
|
||||
|
||||
@when(re.compile(r'I pay (?P<euro>\d+) Euro'))
|
||||
def i_pay(euro, values, request):
|
||||
assert euro == values.pop(0)
|
||||
|
||||
|
||||
@then(re.compile(r'I should have (?P<euro>\d+) Euro'))
|
||||
def i_should_have(euro, values):
|
||||
assert euro == values.pop(0)
|
||||
|
||||
|
||||
def test_multiple_given(request):
|
||||
"""Using the same given fixture raises an error."""
|
||||
test = scenario(
|
||||
'args_steps.feature',
|
||||
'Using the same given fixture raises an error',
|
||||
)
|
||||
with pytest.raises(GivenAlreadyUsed):
|
||||
test(request)
|
|
@ -1,7 +1,9 @@
|
|||
Scenario: Multiple given alias is not evaluated multiple times
|
||||
Given I have an empty list
|
||||
And I have foo (which is 1) in my list
|
||||
|
||||
# Alias of the "I have foo (which is 1) in my list"
|
||||
And I have bar (alias of foo) in my list
|
||||
|
||||
When I do crash (which is 2)
|
||||
And I do boom (alias of crash)
|
||||
Then my list should be [1, 2, 2]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
Scenario: Given after Then
|
||||
Given something
|
||||
When something else
|
||||
Then nevermind
|
||||
Given won't work
|
|
@ -0,0 +1,4 @@
|
|||
Scenario: Given after When
|
||||
Given something
|
||||
When something else
|
||||
Given won't work
|
|
@ -0,0 +1,4 @@
|
|||
Scenario: Some scenario
|
||||
Given 1
|
||||
When 2
|
||||
Then 3
|
|
@ -0,0 +1,4 @@
|
|||
Scenario: Parametrized given, when, thens
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
|
@ -1,6 +1,8 @@
|
|||
Scenario: Given and when using the same fixture should not evaluate it twice
|
||||
Given I have an empty list
|
||||
And I have a fixture (appends 1 to a list)
|
||||
|
||||
# Alias of the "I have a fixture (appends 1 to a list)"
|
||||
And I have a fixture (appends 1 to a list) in reuse syntax
|
||||
|
||||
When I use this fixture
|
||||
Then my list should be [1]
|
||||
|
|
|
@ -21,3 +21,8 @@ Scenario: Then step can follow Given step
|
|||
Scenario: All steps are declared in the conftest
|
||||
Given I have a bar
|
||||
Then bar should have value "bar"
|
||||
|
||||
|
||||
Scenario: Using the same given fixture raises an error
|
||||
Given I have a bar
|
||||
And I have a bar
|
||||
|
|
|
@ -6,7 +6,7 @@ from pytest_bdd import scenario, given, when, then
|
|||
test_steps = scenario('alias.feature', 'Multiple given alias is not evaluated multiple times')
|
||||
|
||||
|
||||
@given('Given I have an empty list')
|
||||
@given('I have an empty list')
|
||||
def results():
|
||||
return []
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import pytest
|
||||
|
||||
from pytest_bdd.scenario import NotEnoughScenarioParams
|
||||
|
||||
from pytest_bdd import given, when, then, scenario
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
['start', 'eat', 'left'],
|
||||
[(12, 5, 7)])
|
||||
@scenario(
|
||||
'parametrized.feature',
|
||||
'Parametrized given, when, thens',
|
||||
)
|
||||
def test_parametrized(request, start, eat, left):
|
||||
"""Test parametrized scenario."""
|
||||
|
||||
|
||||
def test_parametrized_wrongly(request):
|
||||
"""Test parametrized scenario when the test function lacks parameters."""
|
||||
@scenario(
|
||||
'parametrized.feature',
|
||||
'Parametrized given, when, thens',
|
||||
)
|
||||
def wrongly_parametrized(request):
|
||||
pass
|
||||
|
||||
with pytest.raises(NotEnoughScenarioParams) as exc:
|
||||
wrongly_parametrized(request)
|
||||
|
||||
assert exc.value.args == (
|
||||
"""Scenario "Parametrized given, when, thens" in the feature "parametrized.feature" was not able to """
|
||||
"""resolve all declared parameters. """
|
||||
"""Should resolve params: [\'eat\', \'left\', \'start\'], but resolved only: []."""
|
||||
)
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
def start_cucumbers(start):
|
||||
return dict(start=start)
|
||||
|
||||
|
||||
@when('I eat <eat> cucumbers')
|
||||
def eat_cucumbers(start_cucumbers, start, eat):
|
||||
start_cucumbers['eat'] = eat
|
||||
|
||||
|
||||
@then('I should have <left> cucumbers')
|
||||
def should_have_left_cucumbers(start_cucumbers, start, eat, left):
|
||||
assert start - eat == left
|
||||
assert start_cucumbers['start'] == start
|
||||
assert start_cucumbers['eat'] == eat
|
|
@ -0,0 +1,15 @@
|
|||
import pytest
|
||||
|
||||
from pytest_bdd import scenario
|
||||
from pytest_bdd.scenario import ScenarioNotFound
|
||||
|
||||
|
||||
def test_scenario_not_found(request):
|
||||
"""Test the situation when scenario is not found."""
|
||||
test_not_found = scenario(
|
||||
'not_found.feature',
|
||||
'NOT FOUND'
|
||||
)
|
||||
|
||||
with pytest.raises(ScenarioNotFound):
|
||||
test_not_found(request)
|
|
@ -1,4 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.scenario import GivenAlreadyUsed
|
||||
|
||||
|
||||
test_steps = scenario('steps.feature', 'Executed step by step')
|
||||
|
@ -42,12 +45,12 @@ def check_results(results):
|
|||
test_when_first = scenario('steps.feature', 'When step can be the first')
|
||||
|
||||
|
||||
@when('When I do nothing')
|
||||
@when('I do nothing')
|
||||
def do_nothing():
|
||||
pass
|
||||
|
||||
|
||||
@then('Then I make no mistakes')
|
||||
@then('I make no mistakes')
|
||||
def no_errors():
|
||||
assert True
|
||||
|
||||
|
@ -61,3 +64,13 @@ def xyz():
|
|||
return
|
||||
|
||||
test_conftest = scenario('steps.feature', 'All steps are declared in the conftest')
|
||||
|
||||
|
||||
def test_multiple_given(request):
|
||||
"""Using the same given fixture raises an error."""
|
||||
test = scenario(
|
||||
'steps.feature',
|
||||
'Using the same given fixture raises an error',
|
||||
)
|
||||
with pytest.raises(GivenAlreadyUsed):
|
||||
test(request)
|
||||
|
|
|
@ -2,37 +2,70 @@
|
|||
|
||||
import pytest
|
||||
|
||||
from pytest_bdd import scenario
|
||||
from pytest_bdd import scenario, given, when, then
|
||||
from pytest_bdd.feature import FeatureError
|
||||
from pytest_bdd.scenario import StepTypeError
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
'When after then',
|
||||
'Then first',
|
||||
'Given after When',
|
||||
'Given after Then',
|
||||
])
|
||||
def scenario_name(request):
|
||||
return request.param
|
||||
@given('something')
|
||||
def given_something():
|
||||
pass
|
||||
|
||||
|
||||
def test_wrong(request, scenario_name):
|
||||
@when('something else')
|
||||
def when_something_else():
|
||||
pass
|
||||
|
||||
|
||||
@then('nevermind')
|
||||
def then_nevermind():
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('feature', 'scenario_name'),
|
||||
[
|
||||
('when_after_then.feature', 'When after then'),
|
||||
('then_first.feature', 'Then first'),
|
||||
('given_after_when.feature', 'Given after When'),
|
||||
('given_after_then.feature', 'Given after Then'),
|
||||
]
|
||||
)
|
||||
def test_wrong(request, feature, scenario_name):
|
||||
"""Test wrong feature scenarios."""
|
||||
|
||||
sc = scenario('wrong.feature', scenario_name)
|
||||
sc = scenario(feature, scenario_name)
|
||||
with pytest.raises(FeatureError):
|
||||
sc(request)
|
||||
# TODO: assert the exception args from parameters
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'scenario_name',
|
||||
[
|
||||
'When in Given',
|
||||
'When in Then',
|
||||
'Then in Given',
|
||||
'Given in When',
|
||||
'Given in Then',
|
||||
'Then in When',
|
||||
]
|
||||
)
|
||||
def test_wrong_type_order(request, scenario_name):
|
||||
"""Test wrong step type order."""
|
||||
sc = scenario('wrong_type_order.feature', scenario_name)
|
||||
with pytest.raises(StepTypeError) as excinfo:
|
||||
sc(request)
|
||||
excinfo # TODO: assert the exception args from parameters
|
||||
|
||||
|
||||
def test_verbose_output(request):
|
||||
"""Test verbose output of failed feature scenario"""
|
||||
sc = scenario('wrong.feature', 'When after then')
|
||||
try:
|
||||
sc = scenario('when_after_then.feature', 'When after then')
|
||||
with pytest.raises(FeatureError) as excinfo:
|
||||
sc(request)
|
||||
except FeatureError as excinfo:
|
||||
msg, number_of_line, line, prev_mode, mode = excinfo.args
|
||||
|
||||
assert number_of_line == 4
|
||||
assert line == 'When I do it again'
|
||||
assert prev_mode == 'then'
|
||||
assert mode == 'when'
|
||||
msg, line_number, line = excinfo.value.args
|
||||
|
||||
assert line_number == 4
|
||||
assert line == 'When I do it again'
|
|
@ -0,0 +1,2 @@
|
|||
Scenario: Then first
|
||||
Then it won't work
|
|
@ -0,0 +1,6 @@
|
|||
Scenario: When after then
|
||||
Given I don't always write when after then, but
|
||||
When I do
|
||||
Then its fine
|
||||
When I do it again
|
||||
Then its wrong
|
|
@ -1,23 +0,0 @@
|
|||
Scenario: When after then
|
||||
Given I don't always write when after then, but
|
||||
When I do
|
||||
Then its fine
|
||||
When I do it again
|
||||
Then its wrong
|
||||
|
||||
|
||||
Scenario: Then first
|
||||
Then it won't work
|
||||
|
||||
|
||||
Scenario: Given after When
|
||||
Given something
|
||||
When something else
|
||||
Given won't work
|
||||
|
||||
|
||||
Scenario: Given after Then
|
||||
Given something
|
||||
When something else
|
||||
Then nevermind
|
||||
Given won't work
|
|
@ -0,0 +1,24 @@
|
|||
Scenario: When in Given
|
||||
Given something else
|
||||
|
||||
|
||||
Scenario: When in Then
|
||||
When something else
|
||||
Then something else
|
||||
|
||||
|
||||
Scenario: Then in Given
|
||||
Given nevermind
|
||||
|
||||
|
||||
Scenario: Given in When
|
||||
When something
|
||||
|
||||
|
||||
Scenario: Given in Then
|
||||
When something else
|
||||
Then something
|
||||
|
||||
|
||||
Scenario: Then in When
|
||||
When nevermind
|
|
@ -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,21 @@
|
|||
[tox]
|
||||
distshare={homedir}/.tox/distshare
|
||||
envlist=py26,py27,py27-xdist,py33
|
||||
indexserver=
|
||||
pypi = https://pypi.python.org/simple
|
||||
|
||||
[testenv]
|
||||
commands= py.test tests --pep8 --junitxml={envlogdir}/junit-{envname}.xml
|
||||
deps = -r{toxinidir}/requirements-testing.txt
|
||||
|
||||
[testenv:py27-coverage]
|
||||
commands= py.test tests --cov=pytest_bdd --pep8 --junitxml={envlogdir}/junit-{envname}.xml
|
||||
|
||||
[testenv:py27-xdist]
|
||||
basepython=python2.7
|
||||
commands=
|
||||
py.test tests -n3 -rfsxX \
|
||||
--junitxml={envlogdir}/junit-{envname}.xml
|
||||
|
||||
[pytest]
|
||||
pep8maxlinelength=120
|
Loading…
Reference in New Issue