cleanup unnecessary code, move to more clean implementation

This commit is contained in:
Anatoly Bubenkov 2013-08-11 23:34:06 +02:00
parent 3011f19ab3
commit 1f9bbc8cde
7 changed files with 154 additions and 83 deletions

100
README.md
View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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))

View File

@ -1,3 +1,4 @@
mock mock
pytest-pep8 pytest-pep8
pytest-cov pytest-cov
pytest-cache

View File

@ -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

View File

@ -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')