forked from test_framework/pytest-bdd
cleanup unnecessary code, move to more clean implementation
This commit is contained in:
parent
3011f19ab3
commit
1f9bbc8cde
100
README.md
100
README.md
|
@ -9,7 +9,7 @@ BDD library for the py.test runner
|
||||||
Install pytest-bdd
|
Install pytest-bdd
|
||||||
=================
|
=================
|
||||||
|
|
||||||
pip install pytest-bdd
|
pip install pytest-bdd
|
||||||
|
|
||||||
|
|
||||||
Example
|
Example
|
||||||
|
@ -28,36 +28,36 @@ publish_article.feature:
|
||||||
|
|
||||||
test_publish_article.py:
|
test_publish_article.py:
|
||||||
|
|
||||||
from pytest_bdd import scenario, given, when, then
|
from pytest_bdd import scenario, given, when, then
|
||||||
|
|
||||||
test_publish = scenario('publish_article.feature', 'Publishing the article')
|
test_publish = scenario('publish_article.feature', 'Publishing the article')
|
||||||
|
|
||||||
|
|
||||||
@given('I have an article')
|
@given('I have an article')
|
||||||
def article(author):
|
def article(author):
|
||||||
return create_test_article(author=author)
|
return create_test_article(author=author)
|
||||||
|
|
||||||
|
|
||||||
@when('I go to the article page')
|
@when('I go to the article page')
|
||||||
def go_to_article(article, browser):
|
def go_to_article(article, browser):
|
||||||
browser.visit(urljoin(browser.url, '/manage/articles/{0}/'.format(article.id)))
|
browser.visit(urljoin(browser.url, '/manage/articles/{0}/'.format(article.id)))
|
||||||
|
|
||||||
|
|
||||||
@when('I press the publish button')
|
@when('I press the publish button')
|
||||||
def publish_article(browser):
|
def publish_article(browser):
|
||||||
browser.find_by_css('button[name=publish]').first.click()
|
browser.find_by_css('button[name=publish]').first.click()
|
||||||
|
|
||||||
|
|
||||||
@then('I should not see the error message')
|
@then('I should not see the error message')
|
||||||
def no_error_message(browser):
|
def no_error_message(browser):
|
||||||
with pytest.raises(ElementDoesNotExist):
|
with pytest.raises(ElementDoesNotExist):
|
||||||
browser.find_by_css('.message.error').first
|
browser.find_by_css('.message.error').first
|
||||||
|
|
||||||
|
|
||||||
@then('And the article should be published')
|
@then('And the article should be published')
|
||||||
def article_is_published(article):
|
def article_is_published(article):
|
||||||
article.refresh() # Refresh the object in the SQLAlchemy session
|
article.refresh() # Refresh the object in the SQLAlchemy session
|
||||||
assert article.is_published
|
assert article.is_published
|
||||||
|
|
||||||
|
|
||||||
Step aliases
|
Step aliases
|
||||||
|
@ -69,24 +69,24 @@ In order to use the same step function with multiple step names simply
|
||||||
decorate it multiple times:
|
decorate it multiple times:
|
||||||
|
|
||||||
|
|
||||||
@given('I have an article')
|
@given('I have an article')
|
||||||
@given('there\'s an article')
|
@given('there\'s an article')
|
||||||
def article(author):
|
def article(author):
|
||||||
return create_test_article(author=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
|
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.
|
author of the article, but article should have some default author.
|
||||||
|
|
||||||
Scenario: I'm the author
|
Scenario: I'm the author
|
||||||
Given I'm an author
|
Given I'm an author
|
||||||
And I have an article
|
And I have an article
|
||||||
|
|
||||||
|
|
||||||
Scenario: I'm the admin
|
Scenario: I'm the admin
|
||||||
Given I'm the admin
|
Given I'm the admin
|
||||||
And there is an article
|
And there is an article
|
||||||
|
|
||||||
|
|
||||||
Step parameters
|
Step parameters
|
||||||
|
@ -108,16 +108,17 @@ As you can see we don't use Scenario Outline, but use just Scenario, just becaus
|
||||||
|
|
||||||
The code will look like:
|
The code will look like:
|
||||||
|
|
||||||
test_reuse = scenario(
|
# here we use pytest power to parametrize test
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
['start', 'eat', 'left'],
|
||||||
|
[(12, 5, 7)])
|
||||||
|
@scenario(
|
||||||
'parametrized.feature',
|
'parametrized.feature',
|
||||||
'Parametrized given, when, thens',
|
'Parametrized given, when, thens',
|
||||||
# here we tell scenario about the parameters, it's not possible to get them automatically, as
|
|
||||||
# feature files are parsed on test runtime, not import time
|
|
||||||
params=['start', 'eat', 'left']
|
|
||||||
)
|
)
|
||||||
|
# note that we should receive same arguments in function that we use for test parametrization
|
||||||
# here we use pytest power to parametrize test
|
def test_parametrized(start, eat, left):
|
||||||
test_reuse = pytest.mark.parametrize(['start', 'eat', 'left'], [(12, 5, 7)])(test_reuse)
|
"""We don't need to do anything here, everything will be managed by scenario decorator."""
|
||||||
|
|
||||||
|
|
||||||
@given('there are <start> cucumbers')
|
@given('there are <start> cucumbers')
|
||||||
|
@ -135,13 +136,14 @@ The code will look like:
|
||||||
assert start - eat == left
|
assert start - eat == left
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reuse fixtures
|
Reuse fixtures
|
||||||
================
|
================
|
||||||
|
|
||||||
Sometimes scenarios define new names for the fixture that can be inherited.
|
Sometimes scenarios define new names for the fixture that can be inherited.
|
||||||
Fixtures can be reused with other names using given():
|
Fixtures can be reused with other names using given():
|
||||||
|
|
||||||
given('I have beautiful article', fixture='article')
|
given('I have beautiful article', fixture='article')
|
||||||
|
|
||||||
|
|
||||||
Reuse steps
|
Reuse steps
|
||||||
|
@ -152,28 +154,28 @@ expect them in the child test file.
|
||||||
|
|
||||||
common_steps.feature:
|
common_steps.feature:
|
||||||
|
|
||||||
Scenario: All steps are declared in the conftest
|
Scenario: All steps are declared in the conftest
|
||||||
Given I have a bar
|
Given I have a bar
|
||||||
Then bar should have value "bar"
|
Then bar should have value "bar"
|
||||||
|
|
||||||
|
|
||||||
conftest.py:
|
conftest.py:
|
||||||
|
|
||||||
from pytest_bdd import given, then
|
from pytest_bdd import given, then
|
||||||
|
|
||||||
|
|
||||||
@given('I have a bar')
|
@given('I have a bar')
|
||||||
def bar():
|
def bar():
|
||||||
return 'bar'
|
return 'bar'
|
||||||
|
|
||||||
|
|
||||||
@then('bar should have value "bar"')
|
@then('bar should have value "bar"')
|
||||||
def bar_is_bar(bar):
|
def bar_is_bar(bar):
|
||||||
assert bar == 'bar'
|
assert bar == 'bar'
|
||||||
|
|
||||||
test_common.py:
|
test_common.py:
|
||||||
|
|
||||||
test_conftest = scenario('common_steps.feature', 'All steps are declared in the conftest')
|
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
|
There are no definitions of the steps in the test file. They were collected from the parent
|
||||||
|
|
|
@ -43,7 +43,7 @@ STEP_PREFIXES = {
|
||||||
|
|
||||||
COMMENT_SYMBOLS = '#'
|
COMMENT_SYMBOLS = '#'
|
||||||
|
|
||||||
STEP_PARAM_RE = re.compile('\<(.+)\>')
|
STEP_PARAM_RE = re.compile('\<(.+?)\>')
|
||||||
|
|
||||||
|
|
||||||
def get_step_type(line):
|
def get_step_type(line):
|
||||||
|
|
|
@ -13,37 +13,56 @@ test_publish_article = scenario(
|
||||||
import inspect # pragma: no cover
|
import inspect # pragma: no cover
|
||||||
from os import path as op # pragma: no cover
|
from os import path as op # pragma: no cover
|
||||||
|
|
||||||
|
from _pytest import python
|
||||||
|
|
||||||
from pytest_bdd.feature import Feature # pragma: no cover
|
from pytest_bdd.feature import Feature # pragma: no cover
|
||||||
from pytest_bdd.steps import recreate_function # pragma: no cover
|
from pytest_bdd.steps import recreate_function
|
||||||
|
|
||||||
|
|
||||||
class ScenarioNotFound(Exception): # pragma: no cover
|
class ScenarioNotFound(Exception): # pragma: no cover
|
||||||
"""Scenario Not Found"""
|
"""Scenario Not Found"""
|
||||||
|
|
||||||
|
|
||||||
def scenario(feature_name, scenario_name, params=()):
|
def scenario(feature_name, scenario_name):
|
||||||
"""Scenario."""
|
"""Scenario. May be called both as decorator and as just normal function"""
|
||||||
|
|
||||||
def _scenario(request):
|
def decorator(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)
|
|
||||||
|
|
||||||
# Get the scenario
|
def _scenario(request):
|
||||||
try:
|
# Get the feature
|
||||||
scenario = feature.scenarios[scenario_name]
|
base_path = request.getfuncargvalue('pytestbdd_feature_base_dir')
|
||||||
except KeyError:
|
feature_path = op.abspath(op.join(base_path, feature_name))
|
||||||
raise ScenarioNotFound('Scenario "{0}" in feature "{1}" is not found'.format(scenario_name, feature_name))
|
feature = Feature.get_feature(feature_path)
|
||||||
|
|
||||||
# Execute scenario's steps
|
# Get the scenario
|
||||||
for step in scenario.steps:
|
try:
|
||||||
func = request.getfuncargvalue(step)
|
scenario = feature.scenarios[scenario_name]
|
||||||
kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(func).args)
|
except KeyError:
|
||||||
func(**kwargs)
|
raise ScenarioNotFound(
|
||||||
|
'Scenario "{0}" in feature "{1}" is not found'.format(scenario_name, feature_name))
|
||||||
|
|
||||||
if params:
|
if scenario.params != _scenario.pytestbdd_params:
|
||||||
# add test parameters to function
|
raise Exception(scenario.params, _scenario.pytestbdd_params)
|
||||||
_scenario = recreate_function(_scenario, add_args=params)
|
|
||||||
|
|
||||||
return _scenario
|
# Execute scenario's steps
|
||||||
|
for step in scenario.steps:
|
||||||
|
step_func = request.getfuncargvalue(step)
|
||||||
|
kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(step_func).args)
|
||||||
|
step_func(**kwargs)
|
||||||
|
|
||||||
|
_scenario.pytestbdd_params = set()
|
||||||
|
|
||||||
|
if isinstance(request, python.FixtureRequest):
|
||||||
|
# we called as a normal function
|
||||||
|
return _scenario(request)
|
||||||
|
|
||||||
|
# we called as a decorator, so 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, add_args=func_args)
|
||||||
|
_scenario.pytestbdd_params = set(func_args)
|
||||||
|
|
||||||
|
return _scenario
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
|
@ -38,7 +38,7 @@ import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pytest_bdd.feature import remove_prefix
|
from pytest_bdd.feature import remove_prefix, get_step_params
|
||||||
from pytest_bdd.types import GIVEN, WHEN, THEN
|
from pytest_bdd.types import GIVEN, WHEN, THEN
|
||||||
|
|
||||||
PY3 = sys.version_info[0] >= 3
|
PY3 = sys.version_info[0] >= 3
|
||||||
|
@ -48,6 +48,10 @@ class StepError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotEnoughStepParams(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def given(name, fixture=None):
|
def given(name, fixture=None):
|
||||||
"""Given step decorator.
|
"""Given step decorator.
|
||||||
|
|
||||||
|
@ -95,11 +99,10 @@ def _not_a_fixture_decorator(func):
|
||||||
raise StepError('Cannot be used as a decorator when the fixture is specified')
|
raise StepError('Cannot be used as a decorator when the fixture is specified')
|
||||||
|
|
||||||
|
|
||||||
def _step_decorator(step_type, step_name, params=None):
|
def _step_decorator(step_type, step_name):
|
||||||
"""Step decorator for the type and the name.
|
"""Step decorator for the type and the name.
|
||||||
:param step_type: Step type (GIVEN, WHEN or THEN).
|
:param step_type: Step type (GIVEN, WHEN or THEN).
|
||||||
:param step_name: Step name as in the feature file.
|
:param step_name: Step name as in the feature file.
|
||||||
:param params: Step params.
|
|
||||||
|
|
||||||
:return: Decorator function for the step.
|
:return: Decorator function for the step.
|
||||||
|
|
||||||
|
@ -108,8 +111,17 @@ def _step_decorator(step_type, step_name, params=None):
|
||||||
"""
|
"""
|
||||||
step_name = remove_prefix(step_name)
|
step_name = remove_prefix(step_name)
|
||||||
|
|
||||||
|
step_params = set(get_step_params(step_name))
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
step_func = 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, step_params, step_func_args))
|
||||||
|
|
||||||
if step_type == GIVEN:
|
if step_type == GIVEN:
|
||||||
if not hasattr(func, '_pytestfixturefunction'):
|
if not hasattr(func, '_pytestfixturefunction'):
|
||||||
# avoid overfixturing of a fixture
|
# avoid overfixturing of a fixture
|
||||||
|
@ -158,7 +170,8 @@ def recreate_function(func, module=None, add_args=()):
|
||||||
elif arg == 'co_argcount':
|
elif arg == 'co_argcount':
|
||||||
args.append(getattr(code, arg) + len(add_args))
|
args.append(getattr(code, arg) + len(add_args))
|
||||||
elif arg == 'co_varnames':
|
elif arg == 'co_varnames':
|
||||||
args.append(tuple(add_args) + getattr(code, arg))
|
co_varnames = getattr(code, arg)
|
||||||
|
args.append(co_varnames[:code.co_argcount] + tuple(add_args) + co_varnames[code.co_argcount:])
|
||||||
else:
|
else:
|
||||||
args.append(getattr(code, arg))
|
args.append(getattr(code, arg))
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
mock
|
mock
|
||||||
pytest-pep8
|
pytest-pep8
|
||||||
pytest-cov
|
pytest-cov
|
||||||
|
pytest-cache
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
Scenario: Parametrized given, when, thens
|
Scenario: Parametrized given, when, thens
|
||||||
Given there are <start> cucumbers
|
Given there are <start> cucumbers
|
||||||
When I eat <eat> cucumbers
|
When I eat <eat> cucumbers
|
||||||
Then I should have <left> cucumbers
|
Then I should have <left> cucumbers
|
||||||
|
|
|
@ -1,16 +1,52 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pytest_bdd.steps import when
|
from pytest_bdd.steps import NotEnoughStepParams
|
||||||
|
|
||||||
from pytest_bdd import given, then, scenario
|
from pytest_bdd import given, when, then, scenario
|
||||||
|
|
||||||
test_reuse = scenario(
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
['start', 'eat', 'left'],
|
||||||
|
[(12, 5, 7)])
|
||||||
|
@scenario(
|
||||||
'parametrized.feature',
|
'parametrized.feature',
|
||||||
'Parametrized given, when, thens',
|
'Parametrized given, when, thens',
|
||||||
params=['start', 'eat', 'left']
|
|
||||||
)
|
)
|
||||||
|
def test_parametrized(request, start, eat, left):
|
||||||
|
"""Test parametrized scenario."""
|
||||||
|
|
||||||
test_reuse = pytest.mark.parametrize(['start', 'eat', 'left'], [(12, 5, 7)])(test_reuse)
|
|
||||||
|
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: set([\'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: set([\'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: set([\'some\']), but declared only: []',)
|
||||||
|
|
||||||
|
|
||||||
@given('there are <start> cucumbers')
|
@given('there are <start> cucumbers')
|
||||||
|
|
Loading…
Reference in New Issue