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
|
||||
=================
|
||||
|
||||
pip install pytest-bdd
|
||||
pip install pytest-bdd
|
||||
|
||||
|
||||
Example
|
||||
|
@ -28,36 +28,36 @@ publish_article.feature:
|
|||
|
||||
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')
|
||||
def article(author):
|
||||
return create_test_article(author=author)
|
||||
@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 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()
|
||||
@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('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
|
||||
@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
|
||||
|
@ -69,24 +69,24 @@ 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)
|
||||
@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 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
|
||||
Scenario: I'm the admin
|
||||
Given I'm the admin
|
||||
And there is an article
|
||||
|
||||
|
||||
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:
|
||||
|
||||
test_reuse = scenario(
|
||||
# here we use pytest power to parametrize test
|
||||
@pytest.mark.parametrize(
|
||||
['start', 'eat', 'left'],
|
||||
[(12, 5, 7)])
|
||||
@scenario(
|
||||
'parametrized.feature',
|
||||
'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']
|
||||
)
|
||||
|
||||
# here we use pytest power to parametrize test
|
||||
test_reuse = pytest.mark.parametrize(['start', 'eat', 'left'], [(12, 5, 7)])(test_reuse)
|
||||
# note that we should receive same arguments in function that we use for test parametrization
|
||||
def test_parametrized(start, eat, left):
|
||||
"""We don't need to do anything here, everything will be managed by scenario decorator."""
|
||||
|
||||
|
||||
@given('there are <start> cucumbers')
|
||||
|
@ -135,13 +136,14 @@ The code will look like:
|
|||
assert start - eat == left
|
||||
|
||||
|
||||
|
||||
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')
|
||||
given('I have beautiful article', fixture='article')
|
||||
|
||||
|
||||
Reuse steps
|
||||
|
@ -152,28 +154,28 @@ 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"
|
||||
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
|
||||
from pytest_bdd import given, then
|
||||
|
||||
|
||||
@given('I have a bar')
|
||||
def bar():
|
||||
return 'bar'
|
||||
@given('I have a bar')
|
||||
def bar():
|
||||
return 'bar'
|
||||
|
||||
|
||||
@then('bar should have value "bar"')
|
||||
def bar_is_bar(bar):
|
||||
assert bar == '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')
|
||||
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
|
||||
|
|
|
@ -43,7 +43,7 @@ STEP_PREFIXES = {
|
|||
|
||||
COMMENT_SYMBOLS = '#'
|
||||
|
||||
STEP_PARAM_RE = re.compile('\<(.+)\>')
|
||||
STEP_PARAM_RE = re.compile('\<(.+?)\>')
|
||||
|
||||
|
||||
def get_step_type(line):
|
||||
|
|
|
@ -13,37 +13,56 @@ test_publish_article = scenario(
|
|||
import inspect # 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.steps import recreate_function # pragma: no cover
|
||||
from pytest_bdd.steps import recreate_function
|
||||
|
||||
|
||||
class ScenarioNotFound(Exception): # pragma: no cover
|
||||
"""Scenario Not Found"""
|
||||
|
||||
|
||||
def scenario(feature_name, scenario_name, params=()):
|
||||
"""Scenario."""
|
||||
def scenario(feature_name, scenario_name):
|
||||
"""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))
|
||||
|
||||
if params:
|
||||
# add test parameters to function
|
||||
_scenario = recreate_function(_scenario, add_args=params)
|
||||
if scenario.params != _scenario.pytestbdd_params:
|
||||
raise Exception(scenario.params, _scenario.pytestbdd_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
|
||||
|
||||
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
|
||||
|
||||
PY3 = sys.version_info[0] >= 3
|
||||
|
@ -48,6 +48,10 @@ class StepError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class NotEnoughStepParams(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def given(name, fixture=None):
|
||||
"""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')
|
||||
|
||||
|
||||
def _step_decorator(step_type, step_name, params=None):
|
||||
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.
|
||||
:param params: Step params.
|
||||
|
||||
: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_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, step_params, step_func_args))
|
||||
|
||||
if step_type == GIVEN:
|
||||
if not hasattr(func, '_pytestfixturefunction'):
|
||||
# avoid overfixturing of a fixture
|
||||
|
@ -158,7 +170,8 @@ def recreate_function(func, module=None, add_args=()):
|
|||
elif arg == 'co_argcount':
|
||||
args.append(getattr(code, arg) + len(add_args))
|
||||
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:
|
||||
args.append(getattr(code, arg))
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
mock
|
||||
pytest-pep8
|
||||
pytest-cov
|
||||
pytest-cov
|
||||
pytest-cache
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Scenario: Parametrized given, when, thens
|
||||
Given there are <start> cucumbers
|
||||
When I eat <eat> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
Then I should have <left> cucumbers
|
||||
|
|
|
@ -1,16 +1,52 @@
|
|||
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 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')
|
||||
|
|
Loading…
Reference in New Issue