From bca5206677480a7ccef6a4f698f51fca73d47dbc Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Tue, 11 Mar 2014 15:05:25 +0100 Subject: [PATCH 01/11] Scenario outline implementation based on pure pytest parametrization --- CHANGES.rst | 7 + README.rst | 65 +++++---- pytest_bdd/__init__.py | 4 +- pytest_bdd/feature.py | 11 +- pytest_bdd/mark.py | 26 ++++ pytest_bdd/scenario.py | 185 ++++++++++++++++--------- pytest_bdd/steps.py | 30 ++-- setup.py | 3 +- tests/args/args_steps.feature | 14 +- tests/args/test_args_steps.py | 16 +-- tests/feature/outline.feature | 21 +++ tests/feature/test_feature_base_dir.py | 4 +- tests/feature/test_outline.py | 42 +++++- tests/feature/test_parametrized.py | 31 +---- tests/feature/test_scenario.py | 9 +- tests/feature/test_steps.py | 14 +- tests/feature/test_wrong.py | 6 +- 17 files changed, 314 insertions(+), 174 deletions(-) create mode 100644 pytest_bdd/mark.py diff --git a/CHANGES.rst b/CHANGES.rst index 4bf6b71..a214d99 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Changelog ========= +2.0.0 +----- + +- Pure pytest parametrization for scenario outlines (bubenkoff) +- Splitting scenario decorated and non-decorated variants (bubenkoff) + + 1.0.0 ----- diff --git a/README.rst b/README.rst index c093374..a14903a 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,7 @@ mentioned in the feature steps with dependency injection, which allows a true BD just-enough specification of the requirements without maintaining any context object containing the side effects of the Gherkin imperative declarations. + Install pytest-bdd ================== @@ -28,6 +29,7 @@ Install pytest-bdd pip install pytest-bdd + Example ======= @@ -81,6 +83,7 @@ test\_publish\_article.py: article.refresh() # Refresh the object in the SQLAlchemy session assert article.is_published + Step aliases ============ @@ -113,6 +116,7 @@ default author. Given I'm the admin And there is an article + Step arguments ============== @@ -143,28 +147,32 @@ The code will look like: test_arguments = scenario('arguments.feature', 'Arguments for given, when, thens') - @given(re.compile('there are (?P\d+) cucumbers')) + @given(re.compile('there are (?P\d+) cucumbers'), converters=dict(start=int)) 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\d+) cucumbers')) + @when(re.compile('I eat (?P\d+) cucumbers'), converters=dict(eat=int)) def eat_cucumbers(start_cucumbers, eat): - eat = int(eat) start_cucumbers['eat'] += eat - @then(re.compile('I should have (?P\d+) cucumbers')) + @then(re.compile('I should have (?P\d+) cucumbers'), converters=dict(left=int)) 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 +Example code also shows possibility to pass argument converters which may be useful if you need argument types +different than strings. + + Scenario parameters =================== -Scenario can accept `encoding` param to decode content of feature file in specific encoding. UTF-8 is default. +Scenario function/decorator can accept such optional keyword arguments: + + * `encoding` - decode content of feature file in specific encoding. UTF-8 is default. + * `example_converters` - mapping to pass functions to convert example values provided in feature files. + Scenario outlines ================= @@ -174,7 +182,6 @@ templates are written using corner braces as . `Scenario outlines `_ are supported by pytest-bdd exactly as it's described in be behave docs. - Example: .. code-block:: feature @@ -199,42 +206,47 @@ The code will look like: test_outlined = scenario( 'outline.feature', 'Outlined given, when, thens', + example_converters=dict(start=int, eat=float, left=str) ) @given('there are cucumbers') def start_cucumbers(start): - return dict(start=int(start)) + assert isinstance(start, int) + return dict(start=start) @when('I eat cucumbers') - def eat_cucumbers(start_cucumbers, start, eat): - start_cucumbers['eat'] = int(eat) + def eat_cucumbers(start_cucumbers, eat): + assert isinstance(eat, float) + start_cucumbers['eat'] = eat @then('I should have cucumbers') def should_have_left_cucumbers(start_cucumbers, start, eat, left): - assert int(start) - int(eat) == int(left) - assert start_cucumbers['start'] == int(start) - assert start_cucumbers['eat'] == int(eat) + assert isinstance(left, str) + assert start - eat == int(left) + assert start_cucumbers['start'] == start + assert start_cucumbers['eat'] == eat -It's also possible to parametrize the scenario on the python side. This is done 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. +Example code also shows possibility to pass example converters which may be useful if you need parameter types +different than strings. + +It's also possible to parametrize the scenario on the python side. +The reason for this is that it is sometimes not needed to mention example table for every scenario. The code will look like: .. code-block:: python import pytest - from pytest_bdd import scenario, given, when, then + from pytest_bdd import mark, 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( + @mark.scenario( 'parametrized.feature', 'Parametrized given, when, thens', ) @@ -453,6 +465,7 @@ test\_publish\_article.py: You can learn more about `functools.partial `_ in the Python docs. + Hooks ===== @@ -474,15 +487,13 @@ which might be helpful building useful reporting, visualization, etc on top of i * pytest_bdd_step_func_lookup_error(request, feature, scenario, step, exception) - Called when step lookup failed -Subplugins -========== +Browser testing +=============== -The pytest BDD has plugin support, and the main purpose of plugins -(subplugins) is to provide useful and specialized fixtures. +Tools recommended to use for browser testing: -List of known subplugins: + * pytest-splinter - pytest splinter integration for the real browser testing - * pytest-bdd-splinter - collection of fixtures for the real browser BDD testing License ======= diff --git a/pytest_bdd/__init__.py b/pytest_bdd/__init__.py index 26f7698..11f7b49 100644 --- a/pytest_bdd/__init__.py +++ b/pytest_bdd/__init__.py @@ -1,5 +1,5 @@ from pytest_bdd.steps import given, when, then # pragma: no cover from pytest_bdd.scenario import scenario # pragma: no cover +from pytest_bdd import mark # pragma: no cover - -__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__] # pragma: no cover +__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, mark.__name__] # pragma: no cover diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index c1bcd13..b933422 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -229,8 +229,9 @@ class Scenario(object): :param step_type: Step type. """ - self.params.update(get_step_params(step_name)) - self.steps.append(Step(name=step_name, type=step_type)) + params = get_step_params(step_name) + self.params.update(params) + self.steps.append(Step(name=step_name, type=step_type, params=params)) def set_param_names(self, keys): """Set parameter names. @@ -238,8 +239,7 @@ class Scenario(object): :param names: `list` of `string` parameter names """ - self.params.update(keys) - self.example_params = keys + self.example_params = [str(key) for key in keys] def add_example(self, values): """Add example. @@ -253,6 +253,7 @@ class Scenario(object): class Step(object): """Step.""" - def __init__(self, name, type): + def __init__(self, name, type, params): self.name = name self.type = type + self.params = params diff --git a/pytest_bdd/mark.py b/pytest_bdd/mark.py new file mode 100644 index 0000000..5fb7449 --- /dev/null +++ b/pytest_bdd/mark.py @@ -0,0 +1,26 @@ +"""Pytest-bdd markers.""" +import inspect + +from pytest_bdd.steps import recreate_function, get_caller_module, get_caller_function + +from pytest_bdd import scenario as bdd_scenario + + +def scenario(feature_name, scenario_name, encoding='utf-8', example_converters=None): + """Scenario. May be called both as decorator and as just normal function.""" + + caller_module = get_caller_module() + caller_function = get_caller_function() + + def decorator(request): + _scenario = bdd_scenario( + feature_name, scenario_name, encoding=encoding, example_converters=example_converters, + caller_module=caller_module, caller_function=caller_function) + + args = inspect.getargspec(request).args + + _scenario = recreate_function(_scenario, name=request.__name__, module=caller_module, add_args=args) + + return _scenario + + return recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 35c9bf9..eaeea31 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -11,15 +11,26 @@ test_publish_article = scenario( ) """ +import collections +import os +import imp + +import sys import inspect # pragma: no cover from os import path as op # pragma: no cover +import pytest + +from future import utils as future_utils + from _pytest import python from pytest_bdd.feature import Feature, force_encode # pragma: no cover from pytest_bdd.steps import recreate_function, get_caller_module, get_caller_function from pytest_bdd.types import GIVEN +from pytest_bdd import plugin + class ScenarioValidationError(Exception): """Base class for scenario validation.""" @@ -29,8 +40,8 @@ class ScenarioNotFound(ScenarioValidationError): # pragma: no cover """Scenario Not Found""" -class NotEnoughScenarioParams(ScenarioValidationError): # pragma: no cover - """Scenario function doesn't take enough parameters in the arguments.""" +class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover + """Scenario steps argumets do not match declared scenario examples.""" class StepTypeError(ScenarioValidationError): # pragma: no cover @@ -101,7 +112,10 @@ def _find_step_function(request, name, encoding): match = pattern.match(name) if pattern else None if match: + converters = getattr(fixturedef.func, 'converters', {}) for arg, value in match.groupdict().items(): + if arg in converters: + value = converters[arg](value) _inject_fixture(request, arg, value) return request.getfuncargvalue(pattern.pattern) raise @@ -109,26 +123,58 @@ def _find_step_function(request, name, encoding): def _validate_scenario(feature, scenario, request): """Validate the 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.filename, sorted(scenario.params), sorted(resolved_params), + if scenario.params and scenario.example_params and scenario.params != set(scenario.example_params): + raise ScenarioExamplesNotValidError( + """Scenario "{0}" in the feature "{1}" has not valid examples. """ + """Set of step parameters {2} should match set of example values {3}.""".format( + scenario.name, feature.filename, sorted(scenario.params), sorted(scenario.example_params), ) ) -def _execute_scenario_outline(feature, scenario, request, encoding): +def _execute_scenario_outline(feature, scenario, request, encoding, example_converters=None): """Execute the scenario outline.""" + errors = [] + # tricky part, basically here we clear pytest request cache for example in scenario.examples: - for key, value in dict(zip(scenario.example_params, example)).items(): + request._funcargs = {} + request._arg2index = {} + try: + _execute_scenario(feature, scenario, request, encoding, example=dict(zip(scenario.example_params, example))) + except Exception as e: + errors.append([e, sys.exc_info()[2]]) + for error in errors: + raise future_utils.raise_with_traceback(error[0], error[1]) + + +def _execute_step_function(request, feature, step, step_func, example=None): + """Execute step function.""" + kwargs = {} + if example: + for key in step.params: + value = example[key] + if step_func.converters and key in step_func.converters: + value = step_func.converters[key](value) _inject_fixture(request, key, value) - _execute_scenario(feature, scenario, request, encoding) + try: + # Get the step argument values + kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(step_func).args) + request.config.hook.pytest_bdd_before_step( + request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, + step_func_args=kwargs) + # Execute the step + step_func(**kwargs) + request.config.hook.pytest_bdd_after_step( + request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, + step_func_args=kwargs) + except Exception as exception: + request.config.hook.pytest_bdd_step_error( + request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, + step_func_args=kwargs, exception=exception) + raise -def _execute_scenario(feature, scenario, request, encoding): +def _execute_scenario(feature, scenario, request, encoding, example=None): """Execute the scenario.""" _validate_scenario(feature, scenario, request) @@ -165,68 +211,77 @@ def _execute_scenario(feature, scenario, request, encoding): exception=exception) raise - kwargs = {} - try: - # Get the step argument values - kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(step_func).args) - request.config.hook.pytest_bdd_before_step( - request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, - step_func_args=kwargs) - # Execute the step - step_func(**kwargs) - request.config.hook.pytest_bdd_after_step( - request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, - step_func_args=kwargs) - except Exception as exception: - request.config.hook.pytest_bdd_step_error( - request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, - step_func_args=kwargs, exception=exception) - raise + _execute_step_function(request, feature, step, step_func, example=example) -def scenario(feature_name, scenario_name, encoding='utf-8'): - """Scenario. May be called both as decorator and as just normal function.""" +FakeRequest = collections.namedtuple('FakeRequest', ['module']) - caller_module = get_caller_module() - caller_function = get_caller_function() - def decorator(request): +def get_fixture(caller_module, fixture, path=None, module=None): + """Get first conftest module from given one.""" + def call_fixture(function): + args = [] + if 'request' in inspect.getargspec(function).args: + args = [FakeRequest(module=caller_module)] + return function(*args) - 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, encoding=encoding) + if not module: + module = caller_module - # 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 hasattr(module, fixture): + return call_fixture(getattr(module, fixture)) - if scenario.examples: - _execute_scenario_outline(feature, scenario, request, encoding) - else: - _execute_scenario(feature, scenario, request, encoding) + if path is None: + path = os.path.dirname(module.__file__) + if os.path.exists(os.path.join(path, '__init__.py')): + file_path = os.path.join(path, 'conftest.py') + if os.path.exists(file_path): + conftest = imp.load_source('conftest', file_path) + if hasattr(conftest, fixture): + return get_fixture(caller_module, fixture, module=conftest) + else: + return get_fixture(caller_module, fixture, module=plugin) + return get_fixture(caller_module, fixture, path=os.path.dirname(path), module=module) - _scenario.pytestbdd_params = set() - if isinstance(request, python.FixtureRequest): - # Called as a normal function. - _scenario = recreate_function(_scenario, module=caller_module) - return _scenario(request) +def scenario( + feature_name, scenario_name, encoding='utf-8', example_converters=None, + caller_module=None, caller_function=None): + """Scenario.""" - # 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, module=caller_module) - _scenario.pytestbdd_params = set(func_args) + caller_module = caller_module or get_caller_module() + caller_function = caller_function or get_caller_function() - return _scenario + # Get the feature + base_path = get_fixture(caller_module, 'pytestbdd_feature_base_dir') + feature_path = op.abspath(op.join(base_path, feature_name)) + feature = Feature.get_feature(feature_path, encoding=encoding) - decorator = recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) + # 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 decorator + if scenario.examples: + params = [] + for example in scenario.examples: + for index, param in enumerate(scenario.example_params): + if example_converters and param in example_converters: + example[index] = example_converters[param](example[index]) + params.append(example) + params = [scenario.example_params, params] + else: + params = [] + + def _scenario(request, *args, **kwargs): + _execute_scenario(feature, scenario, request, encoding) + + _scenario = recreate_function( + _scenario, module=caller_module, firstlineno=caller_function.f_lineno, + add_args=scenario.example_params) + if params: + _scenario = pytest.mark.parametrize(*params)(_scenario) + return _scenario diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index e69651a..f5b77e4 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -51,11 +51,13 @@ class StepError(Exception): # pragma: no cover RE_TYPE = type(re.compile('')) # pragma: no cover -def given(name, fixture=None): +def given(name, fixture=None, converters=None): """Given step decorator. :param name: Given step name. :param fixture: Optional name of the fixture to reuse. + :param converters: Optional `dict` of the argument or parameter converters in form + {: }. :raises: StepError in case of wrong configuration. :note: Can't be used as a decorator when the fixture is specified. @@ -66,6 +68,7 @@ def given(name, fixture=None): module = get_caller_module() step_func = lambda request: request.getfuncargvalue(fixture) step_func.step_type = GIVEN + step_func.converters = converters step_func.__name__ = name step_func.fixture = fixture func = pytest.fixture(lambda: step_func) @@ -73,29 +76,33 @@ def given(name, fixture=None): contribute_to_module(module, remove_prefix(name), func) return _not_a_fixture_decorator - return _step_decorator(GIVEN, name) + return _step_decorator(GIVEN, name, converters=converters) -def when(name): +def when(name, converters=None): """When step decorator. :param name: Step name. + :param converters: Optional `dict` of the argument or parameter converters in form + {: }. :raises: StepError in case of wrong configuration. """ - return _step_decorator(WHEN, name) + return _step_decorator(WHEN, name, converters=converters) -def then(name): +def then(name, converters=None): """Then step decorator. :param name: Step name. + :param converters: Optional `dict` of the argument or parameter converters in form + {: }. :raises: StepError in case of wrong configuration. """ - return _step_decorator(THEN, name) + return _step_decorator(THEN, name, converters=converters) def _not_a_fixture_decorator(func): @@ -109,7 +116,7 @@ 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): +def _step_decorator(step_type, step_name, converters=None): """Step decorator for the type and the name. :param step_type: Step type (GIVEN, WHEN or THEN). @@ -141,6 +148,7 @@ def _step_decorator(step_type, step_name): step_func.__name__ = step_name step_func.step_type = step_type + step_func.converters = converters @pytest.fixture def lazy_step_func(): @@ -151,6 +159,8 @@ def _step_decorator(step_type, step_name): if pattern: lazy_step_func.pattern = pattern + if converters: + lazy_step_func.converters = converters contribute_to_module( get_caller_module(), @@ -162,7 +172,7 @@ def _step_decorator(step_type, step_name): return decorator -def recreate_function(func, module=None, name=None, add_args=(), firstlineno=None): +def recreate_function(func, module=None, name=None, add_args=[], firstlineno=None): """Recreate a function, replacing some info. :param func: Function object. @@ -188,6 +198,10 @@ def recreate_function(func, module=None, name=None, add_args=(), firstlineno=Non if PY3: argnames.insert(1, 'co_kwonlyargcount') + for arg in inspect.getargspec(func).args: + if arg in add_args: + add_args.remove(arg) + args = [] code = get_code(func) for arg in argnames: diff --git a/setup.py b/setup.py index 57ea648..5c87d29 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup from setuptools.command.test import test as TestCommand -version = '1.0.0' +version = '2.0.0' class Tox(TestCommand): @@ -54,6 +54,7 @@ setup( cmdclass={'test': Tox}, install_requires=[ 'pytest', + 'future' ], # the following makes a plugin available to py.test entry_points={ diff --git a/tests/args/args_steps.feature b/tests/args/args_steps.feature index 9de6211..2e51949 100644 --- a/tests/args/args_steps.feature +++ b/tests/args/args_steps.feature @@ -1,11 +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... + 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 + Given I have 1 Euro + And I have 2 Euro diff --git a/tests/args/test_args_steps.py b/tests/args/test_args_steps.py index c7c22f7..1c28516 100644 --- a/tests/args/test_args_steps.py +++ b/tests/args/test_args_steps.py @@ -19,19 +19,24 @@ test_argument_in_when_step_2 = sc('Argument in when, step 2') @pytest.fixture def values(): - return ['1', '2', '1', '0', '999999'] + return [1, 2, 1, 0, 999999] -@given(re.compile(r'I have (?P\d+) Euro')) +@given(re.compile(r'I have (?P\d+) Euro'), converters=dict(euro=int)) def i_have(euro, values): assert euro == values.pop(0) -@when(re.compile(r'I pay (?P\d+) Euro')) +@when(re.compile(r'I pay (?P\d+) Euro'), converters=dict(euro=int)) def i_pay(euro, values, request): assert euro == values.pop(0) +@then(re.compile(r'I should have (?P\d+) Euro'), converters=dict(euro=int)) +def i_should_have(euro, values): + assert euro == values.pop(0) + + @given('I have an argument') def argument(): """I have an argument.""" @@ -44,11 +49,6 @@ def get_argument(argument, arg): argument['arg'] = arg -@then(re.compile(r'I should have (?P\d+) Euro')) -def i_should_have(euro, values): - assert euro == values.pop(0) - - @then(re.compile('My argument should be (?P\d+)')) def assert_that_my_argument_is_arg(argument, arg): """Assert that arg from when equals arg.""" diff --git a/tests/feature/outline.feature b/tests/feature/outline.feature index 50a3332..947ae52 100644 --- a/tests/feature/outline.feature +++ b/tests/feature/outline.feature @@ -6,3 +6,24 @@ Scenario Outline: Outlined given, when, thens Examples: | start | eat | left | | 12 | 5 | 7 | + + +Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | unknown_param | + | 12 | 5 | 7 | value | + + +Scenario Outline: Outlined with some examples failing + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | + | 0 | 5 | 5 | + | 12 | 5 | 7 | diff --git a/tests/feature/test_feature_base_dir.py b/tests/feature/test_feature_base_dir.py index e8adcc9..8fb9b51 100644 --- a/tests/feature/test_feature_base_dir.py +++ b/tests/feature/test_feature_base_dir.py @@ -20,8 +20,6 @@ def pytestbdd_feature_base_dir(): def test_feature_path(request, scenario_name): """Test feature base dir.""" - sc = scenario('steps.feature', scenario_name) with pytest.raises(IOError) as exc: - sc(request) - + scenario('steps.feature', scenario_name) assert os.path.join('/does/not/exist/', 'steps.feature') in str(exc.value) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 54d2ab9..edb8fa2 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -1,25 +1,55 @@ """Scenario Outline tests.""" +import re + +import pytest + from pytest_bdd import given, when, then, scenario +from pytest_bdd import mark +from pytest_bdd.scenario import ScenarioExamplesNotValidError test_outlined = scenario( 'outline.feature', 'Outlined given, when, thens', + example_converters=dict(start=int, eat=float, left=str) ) @given('there are cucumbers') def start_cucumbers(start): - return dict(start=int(start)) + assert isinstance(start, int) + return dict(start=start) @when('I eat cucumbers') -def eat_cucumbers(start_cucumbers, start, eat): - start_cucumbers['eat'] = int(eat) +def eat_cucumbers(start_cucumbers, eat): + assert isinstance(eat, float) + start_cucumbers['eat'] = eat @then('I should have cucumbers') def should_have_left_cucumbers(start_cucumbers, start, eat, left): - assert int(start) - int(eat) == int(left) - assert start_cucumbers['start'] == int(start) - assert start_cucumbers['eat'] == int(eat) + assert isinstance(left, str) + assert start - eat == int(left) + assert start_cucumbers['start'] == start + assert start_cucumbers['eat'] == eat + + +def test_wrongly_outlined(request): + """Test parametrized scenario when the test function lacks parameters.""" + @mark.scenario( + 'outline.feature', + 'Outlined with wrong examples', + ) + def wrongly_outlined(request): + pass + + with pytest.raises(ScenarioExamplesNotValidError) as exc: + wrongly_outlined(request, 1, 2, 3, 4) + + assert re.match( + """Scenario \"Outlined with wrong examples\" in the feature \"(.+)\" has not valid examples\. """ + """Set of step parameters (.+) should match set of example values """ + """(.+)\.""", + exc.value.args[0] + ) diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index abebba4..b1af275 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -1,14 +1,12 @@ import pytest -from pytest_bdd.scenario import NotEnoughScenarioParams - -from pytest_bdd import given, when, then, scenario +from pytest_bdd import given, when, then, mark @pytest.mark.parametrize( ['start', 'eat', 'left'], [(12, 5, 7)]) -@scenario( +@mark.scenario( 'parametrized.feature', 'Parametrized given, when, thens', ) @@ -18,37 +16,18 @@ def test_parametrized(request, start, eat, left): @pytest.fixture(params=[1, 2]) def foo_bar(request): - return 'foo_bar' * request.param + return 'bar' * request.param @pytest.mark.parametrize( ['start', 'eat', 'left'], [(12, 5, 7)]) -@scenario( +@mark.scenario( 'parametrized.feature', 'Parametrized given, when, thens', ) def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar): - """Test parametrized scenario, but also with other fixtures.""" - - -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: [].""" - ) + """Test parametrized scenario, but also with other parametrized fixtures.""" @given('there are cucumbers') diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index 86ca1f6..95a7efc 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -6,10 +6,9 @@ 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) + scenario( + 'not_found.feature', + 'NOT FOUND' + ) diff --git a/tests/feature/test_steps.py b/tests/feature/test_steps.py index fd92b61..662a844 100644 --- a/tests/feature/test_steps.py +++ b/tests/feature/test_steps.py @@ -96,7 +96,7 @@ def test_step_hooks(testdir): """) testdir.makepyfile(""" import pytest - from pytest_bdd import given, when, scenario + from pytest_bdd import given, when, mark @given('I have a bar') def i_have_bar(): @@ -118,15 +118,15 @@ def test_step_hooks(testdir): def when_dependency_fails(dependency): pass - @scenario('test.feature', "When step's dependency a has failure") + @mark.scenario('test.feature', "When step's dependency a has failure") def test_when_dependency_fails(): pass - @scenario('test.feature', 'When step has hook on failure') + @mark.scenario('test.feature', 'When step has hook on failure') def test_when_fails(): pass - @scenario('test.feature', 'When step is not found') + @mark.scenario('test.feature', 'When step is not found') def test_when_not_found(): pass @@ -134,7 +134,7 @@ def test_step_hooks(testdir): def foo(): return 'foo' - @scenario('test.feature', 'When step validation error happens') + @mark.scenario('test.feature', 'When step validation error happens') def test_when_step_validation_error(): pass """) @@ -185,7 +185,7 @@ def test_step_trace(testdir): """) testdir.makepyfile(""" import pytest - from pytest_bdd import given, when, scenario + from pytest_bdd import given, when, scenario, mark @given('I have a bar') def i_have_bar(): @@ -197,7 +197,7 @@ def test_step_trace(testdir): test_when_fails_inline = scenario('test.feature', 'When step has failure') - @scenario('test.feature', 'When step has failure') + @mark.scenario('test.feature', 'When step has failure') def test_when_fails_decorated(): pass diff --git a/tests/feature/test_wrong.py b/tests/feature/test_wrong.py index 06b0f26..908c012 100644 --- a/tests/feature/test_wrong.py +++ b/tests/feature/test_wrong.py @@ -33,9 +33,8 @@ def then_nevermind(): def test_wrong(request, feature, scenario_name): """Test wrong feature scenarios.""" - sc = scenario(feature, scenario_name) with pytest.raises(FeatureError): - sc(request) + scenario(feature, scenario_name) # TODO: assert the exception args from parameters @@ -60,9 +59,8 @@ def test_wrong_type_order(request, scenario_name): def test_verbose_output(request): """Test verbose output of failed feature scenario""" - sc = scenario('when_after_then.feature', 'When after then') with pytest.raises(FeatureError) as excinfo: - sc(request) + scenario('when_after_then.feature', 'When after then') msg, line_number, line = excinfo.value.args From 05e3c0e171cbb1c44075449b133fe085ea6b1a16 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Thu, 13 Mar 2014 18:32:49 +0100 Subject: [PATCH 02/11] refactor to use exec --- pytest_bdd/mark.py | 27 ++++++++++++++++++++-- pytest_bdd/scenario.py | 42 +++++++++++++---------------------- pytest_bdd/steps.py | 5 ++++- setup.py | 1 - tests/feature/test_outline.py | 13 +++++------ 5 files changed, 50 insertions(+), 38 deletions(-) diff --git a/pytest_bdd/mark.py b/pytest_bdd/mark.py index 5fb7449..99b8d84 100644 --- a/pytest_bdd/mark.py +++ b/pytest_bdd/mark.py @@ -1,7 +1,9 @@ """Pytest-bdd markers.""" import inspect -from pytest_bdd.steps import recreate_function, get_caller_module, get_caller_function +import pytest + +from pytest_bdd.steps import execute, recreate_function, get_caller_module, get_caller_function from pytest_bdd import scenario as bdd_scenario @@ -19,7 +21,28 @@ def scenario(feature_name, scenario_name, encoding='utf-8', example_converters=N args = inspect.getargspec(request).args - _scenario = recreate_function(_scenario, name=request.__name__, module=caller_module, add_args=args) + if 'request' not in args: + args.insert(0, 'request') + + g = globals().copy() + g.update(locals()) + + pytestbdd_params = _scenario.pytestbdd_params + scenario = _scenario.scenario + + sc_args = list(scenario.example_params) + if 'request' not in sc_args: + sc_args.insert(0, 'request') + + code = """def _decorated_scenario({0}): + _scenario({1})""".format(', '.join(args), ', '.join(sc_args)) + + execute(code, g) + + _scenario = recreate_function(g['_decorated_scenario'], module=caller_module, add_args=args) + + if pytestbdd_params: + _scenario = pytest.mark.parametrize(*pytestbdd_params)(_scenario) return _scenario diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index eaeea31..3279d2f 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -11,22 +11,20 @@ test_publish_article = scenario( ) """ + import collections import os import imp -import sys import inspect # pragma: no cover from os import path as op # pragma: no cover import pytest -from future import utils as future_utils - from _pytest import python from pytest_bdd.feature import Feature, force_encode # pragma: no cover -from pytest_bdd.steps import recreate_function, get_caller_module, get_caller_function +from pytest_bdd.steps import execute, recreate_function, get_caller_module, get_caller_function from pytest_bdd.types import GIVEN from pytest_bdd import plugin @@ -110,7 +108,6 @@ def _find_step_function(request, name, encoding): pattern = getattr(fixturedef.func, 'pattern', None) match = pattern.match(name) if pattern else None - if match: converters = getattr(fixturedef.func, 'converters', {}) for arg, value in match.groupdict().items(): @@ -121,7 +118,7 @@ def _find_step_function(request, name, encoding): raise -def _validate_scenario(feature, scenario, request): +def _validate_scenario(feature, scenario): """Validate the scenario.""" if scenario.params and scenario.example_params and scenario.params != set(scenario.example_params): raise ScenarioExamplesNotValidError( @@ -132,21 +129,6 @@ def _validate_scenario(feature, scenario, request): ) -def _execute_scenario_outline(feature, scenario, request, encoding, example_converters=None): - """Execute the scenario outline.""" - errors = [] - # tricky part, basically here we clear pytest request cache - for example in scenario.examples: - request._funcargs = {} - request._arg2index = {} - try: - _execute_scenario(feature, scenario, request, encoding, example=dict(zip(scenario.example_params, example))) - except Exception as e: - errors.append([e, sys.exc_info()[2]]) - for error in errors: - raise future_utils.raise_with_traceback(error[0], error[1]) - - def _execute_step_function(request, feature, step, step_func, example=None): """Execute step function.""" kwargs = {} @@ -177,8 +159,6 @@ def _execute_step_function(request, feature, step, step_func, example=None): def _execute_scenario(feature, scenario, request, encoding, example=None): """Execute the scenario.""" - _validate_scenario(feature, scenario, request) - givens = set() # Execute scenario steps for step in scenario.steps: @@ -265,6 +245,8 @@ def scenario( 'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name) ) + _validate_scenario(feature, scenario) + if scenario.examples: params = [] for example in scenario.examples: @@ -276,12 +258,18 @@ def scenario( else: params = [] - def _scenario(request, *args, **kwargs): - _execute_scenario(feature, scenario, request, encoding) + g = globals().copy() + g.update(locals()) + + code = """def _scenario(request, {0}): + _execute_scenario(feature, scenario, request, encoding)""".format(','.join(scenario.example_params)) + + execute(code, g) _scenario = recreate_function( - _scenario, module=caller_module, firstlineno=caller_function.f_lineno, - add_args=scenario.example_params) + g['_scenario'], module=caller_module, firstlineno=caller_function.f_lineno) if params: _scenario = pytest.mark.parametrize(*params)(_scenario) + _scenario.pytestbdd_params = params + _scenario.scenario = scenario return _scenario diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index f5b77e4..a31c7c8 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -234,7 +234,6 @@ def contribute_to_module(module, name, func): """ func = recreate_function(func, module=module) - setattr(module, name, func) @@ -247,3 +246,7 @@ def get_caller_module(depth=2): def get_caller_function(depth=2): """Return caller function.""" return sys._getframe(depth) + + +def execute(code, g): + exec(code, g) diff --git a/setup.py b/setup.py index 5c87d29..5e659fc 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,6 @@ setup( cmdclass={'test': Tox}, install_requires=[ 'pytest', - 'future' ], # the following makes a plugin available to py.test entry_points={ diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index edb8fa2..570e9f1 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -37,15 +37,14 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): def test_wrongly_outlined(request): """Test parametrized scenario when the test function lacks parameters.""" - @mark.scenario( - 'outline.feature', - 'Outlined with wrong examples', - ) - def wrongly_outlined(request): - pass with pytest.raises(ScenarioExamplesNotValidError) as exc: - wrongly_outlined(request, 1, 2, 3, 4) + @mark.scenario( + 'outline.feature', + 'Outlined with wrong examples', + ) + def wrongly_outlined(request): + pass assert re.match( """Scenario \"Outlined with wrong examples\" in the feature \"(.+)\" has not valid examples\. """ From e3e4cb6ad2a239dc7aa942d59a6e1003cb207a75 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Tue, 11 Mar 2014 15:05:25 +0100 Subject: [PATCH 03/11] Scenario outline implementation based on pure pytest parametrization --- CHANGES.rst | 7 + README.rst | 65 +++++---- pytest_bdd/__init__.py | 4 +- pytest_bdd/feature.py | 11 +- pytest_bdd/mark.py | 49 +++++++ pytest_bdd/scenario.py | 187 +++++++++++++++---------- pytest_bdd/steps.py | 35 +++-- setup.py | 2 +- tests/args/args_steps.feature | 14 +- tests/args/test_args_steps.py | 16 +-- tests/feature/outline.feature | 21 +++ tests/feature/test_feature_base_dir.py | 4 +- tests/feature/test_outline.py | 41 +++++- tests/feature/test_parametrized.py | 31 +--- tests/feature/test_scenario.py | 9 +- tests/feature/test_steps.py | 14 +- tests/feature/test_wrong.py | 6 +- 17 files changed, 334 insertions(+), 182 deletions(-) create mode 100644 pytest_bdd/mark.py diff --git a/CHANGES.rst b/CHANGES.rst index 4bf6b71..a214d99 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,13 @@ Changelog ========= +2.0.0 +----- + +- Pure pytest parametrization for scenario outlines (bubenkoff) +- Splitting scenario decorated and non-decorated variants (bubenkoff) + + 1.0.0 ----- diff --git a/README.rst b/README.rst index c093374..a14903a 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,7 @@ mentioned in the feature steps with dependency injection, which allows a true BD just-enough specification of the requirements without maintaining any context object containing the side effects of the Gherkin imperative declarations. + Install pytest-bdd ================== @@ -28,6 +29,7 @@ Install pytest-bdd pip install pytest-bdd + Example ======= @@ -81,6 +83,7 @@ test\_publish\_article.py: article.refresh() # Refresh the object in the SQLAlchemy session assert article.is_published + Step aliases ============ @@ -113,6 +116,7 @@ default author. Given I'm the admin And there is an article + Step arguments ============== @@ -143,28 +147,32 @@ The code will look like: test_arguments = scenario('arguments.feature', 'Arguments for given, when, thens') - @given(re.compile('there are (?P\d+) cucumbers')) + @given(re.compile('there are (?P\d+) cucumbers'), converters=dict(start=int)) 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\d+) cucumbers')) + @when(re.compile('I eat (?P\d+) cucumbers'), converters=dict(eat=int)) def eat_cucumbers(start_cucumbers, eat): - eat = int(eat) start_cucumbers['eat'] += eat - @then(re.compile('I should have (?P\d+) cucumbers')) + @then(re.compile('I should have (?P\d+) cucumbers'), converters=dict(left=int)) 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 +Example code also shows possibility to pass argument converters which may be useful if you need argument types +different than strings. + + Scenario parameters =================== -Scenario can accept `encoding` param to decode content of feature file in specific encoding. UTF-8 is default. +Scenario function/decorator can accept such optional keyword arguments: + + * `encoding` - decode content of feature file in specific encoding. UTF-8 is default. + * `example_converters` - mapping to pass functions to convert example values provided in feature files. + Scenario outlines ================= @@ -174,7 +182,6 @@ templates are written using corner braces as . `Scenario outlines `_ are supported by pytest-bdd exactly as it's described in be behave docs. - Example: .. code-block:: feature @@ -199,42 +206,47 @@ The code will look like: test_outlined = scenario( 'outline.feature', 'Outlined given, when, thens', + example_converters=dict(start=int, eat=float, left=str) ) @given('there are cucumbers') def start_cucumbers(start): - return dict(start=int(start)) + assert isinstance(start, int) + return dict(start=start) @when('I eat cucumbers') - def eat_cucumbers(start_cucumbers, start, eat): - start_cucumbers['eat'] = int(eat) + def eat_cucumbers(start_cucumbers, eat): + assert isinstance(eat, float) + start_cucumbers['eat'] = eat @then('I should have cucumbers') def should_have_left_cucumbers(start_cucumbers, start, eat, left): - assert int(start) - int(eat) == int(left) - assert start_cucumbers['start'] == int(start) - assert start_cucumbers['eat'] == int(eat) + assert isinstance(left, str) + assert start - eat == int(left) + assert start_cucumbers['start'] == start + assert start_cucumbers['eat'] == eat -It's also possible to parametrize the scenario on the python side. This is done 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. +Example code also shows possibility to pass example converters which may be useful if you need parameter types +different than strings. + +It's also possible to parametrize the scenario on the python side. +The reason for this is that it is sometimes not needed to mention example table for every scenario. The code will look like: .. code-block:: python import pytest - from pytest_bdd import scenario, given, when, then + from pytest_bdd import mark, 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( + @mark.scenario( 'parametrized.feature', 'Parametrized given, when, thens', ) @@ -453,6 +465,7 @@ test\_publish\_article.py: You can learn more about `functools.partial `_ in the Python docs. + Hooks ===== @@ -474,15 +487,13 @@ which might be helpful building useful reporting, visualization, etc on top of i * pytest_bdd_step_func_lookup_error(request, feature, scenario, step, exception) - Called when step lookup failed -Subplugins -========== +Browser testing +=============== -The pytest BDD has plugin support, and the main purpose of plugins -(subplugins) is to provide useful and specialized fixtures. +Tools recommended to use for browser testing: -List of known subplugins: + * pytest-splinter - pytest splinter integration for the real browser testing - * pytest-bdd-splinter - collection of fixtures for the real browser BDD testing License ======= diff --git a/pytest_bdd/__init__.py b/pytest_bdd/__init__.py index 26f7698..11f7b49 100644 --- a/pytest_bdd/__init__.py +++ b/pytest_bdd/__init__.py @@ -1,5 +1,5 @@ from pytest_bdd.steps import given, when, then # pragma: no cover from pytest_bdd.scenario import scenario # pragma: no cover +from pytest_bdd import mark # pragma: no cover - -__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__] # pragma: no cover +__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, mark.__name__] # pragma: no cover diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index c1bcd13..b933422 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -229,8 +229,9 @@ class Scenario(object): :param step_type: Step type. """ - self.params.update(get_step_params(step_name)) - self.steps.append(Step(name=step_name, type=step_type)) + params = get_step_params(step_name) + self.params.update(params) + self.steps.append(Step(name=step_name, type=step_type, params=params)) def set_param_names(self, keys): """Set parameter names. @@ -238,8 +239,7 @@ class Scenario(object): :param names: `list` of `string` parameter names """ - self.params.update(keys) - self.example_params = keys + self.example_params = [str(key) for key in keys] def add_example(self, values): """Add example. @@ -253,6 +253,7 @@ class Scenario(object): class Step(object): """Step.""" - def __init__(self, name, type): + def __init__(self, name, type, params): self.name = name self.type = type + self.params = params diff --git a/pytest_bdd/mark.py b/pytest_bdd/mark.py new file mode 100644 index 0000000..99b8d84 --- /dev/null +++ b/pytest_bdd/mark.py @@ -0,0 +1,49 @@ +"""Pytest-bdd markers.""" +import inspect + +import pytest + +from pytest_bdd.steps import execute, recreate_function, get_caller_module, get_caller_function + +from pytest_bdd import scenario as bdd_scenario + + +def scenario(feature_name, scenario_name, encoding='utf-8', example_converters=None): + """Scenario. May be called both as decorator and as just normal function.""" + + caller_module = get_caller_module() + caller_function = get_caller_function() + + def decorator(request): + _scenario = bdd_scenario( + feature_name, scenario_name, encoding=encoding, example_converters=example_converters, + caller_module=caller_module, caller_function=caller_function) + + args = inspect.getargspec(request).args + + if 'request' not in args: + args.insert(0, 'request') + + g = globals().copy() + g.update(locals()) + + pytestbdd_params = _scenario.pytestbdd_params + scenario = _scenario.scenario + + sc_args = list(scenario.example_params) + if 'request' not in sc_args: + sc_args.insert(0, 'request') + + code = """def _decorated_scenario({0}): + _scenario({1})""".format(', '.join(args), ', '.join(sc_args)) + + execute(code, g) + + _scenario = recreate_function(g['_decorated_scenario'], module=caller_module, add_args=args) + + if pytestbdd_params: + _scenario = pytest.mark.parametrize(*pytestbdd_params)(_scenario) + + return _scenario + + return recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 35c9bf9..3279d2f 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -11,15 +11,24 @@ test_publish_article = scenario( ) """ + +import collections +import os +import imp + import inspect # pragma: no cover from os import path as op # pragma: no cover +import pytest + from _pytest import python from pytest_bdd.feature import Feature, force_encode # pragma: no cover -from pytest_bdd.steps import recreate_function, get_caller_module, get_caller_function +from pytest_bdd.steps import execute, recreate_function, get_caller_module, get_caller_function from pytest_bdd.types import GIVEN +from pytest_bdd import plugin + class ScenarioValidationError(Exception): """Base class for scenario validation.""" @@ -29,8 +38,8 @@ class ScenarioNotFound(ScenarioValidationError): # pragma: no cover """Scenario Not Found""" -class NotEnoughScenarioParams(ScenarioValidationError): # pragma: no cover - """Scenario function doesn't take enough parameters in the arguments.""" +class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover + """Scenario steps argumets do not match declared scenario examples.""" class StepTypeError(ScenarioValidationError): # pragma: no cover @@ -99,40 +108,57 @@ def _find_step_function(request, name, encoding): pattern = getattr(fixturedef.func, 'pattern', None) match = pattern.match(name) if pattern else None - if match: + converters = getattr(fixturedef.func, 'converters', {}) for arg, value in match.groupdict().items(): + if arg in converters: + value = converters[arg](value) _inject_fixture(request, arg, value) return request.getfuncargvalue(pattern.pattern) raise -def _validate_scenario(feature, scenario, request): +def _validate_scenario(feature, scenario): """Validate the 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.filename, sorted(scenario.params), sorted(resolved_params), + if scenario.params and scenario.example_params and scenario.params != set(scenario.example_params): + raise ScenarioExamplesNotValidError( + """Scenario "{0}" in the feature "{1}" has not valid examples. """ + """Set of step parameters {2} should match set of example values {3}.""".format( + scenario.name, feature.filename, sorted(scenario.params), sorted(scenario.example_params), ) ) -def _execute_scenario_outline(feature, scenario, request, encoding): - """Execute the scenario outline.""" - for example in scenario.examples: - for key, value in dict(zip(scenario.example_params, example)).items(): +def _execute_step_function(request, feature, step, step_func, example=None): + """Execute step function.""" + kwargs = {} + if example: + for key in step.params: + value = example[key] + if step_func.converters and key in step_func.converters: + value = step_func.converters[key](value) _inject_fixture(request, key, value) - _execute_scenario(feature, scenario, request, encoding) + try: + # Get the step argument values + kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(step_func).args) + request.config.hook.pytest_bdd_before_step( + request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, + step_func_args=kwargs) + # Execute the step + step_func(**kwargs) + request.config.hook.pytest_bdd_after_step( + request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, + step_func_args=kwargs) + except Exception as exception: + request.config.hook.pytest_bdd_step_error( + request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, + step_func_args=kwargs, exception=exception) + raise -def _execute_scenario(feature, scenario, request, encoding): +def _execute_scenario(feature, scenario, request, encoding, example=None): """Execute the scenario.""" - _validate_scenario(feature, scenario, request) - givens = set() # Execute scenario steps for step in scenario.steps: @@ -165,68 +191,85 @@ def _execute_scenario(feature, scenario, request, encoding): exception=exception) raise - kwargs = {} - try: - # Get the step argument values - kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(step_func).args) - request.config.hook.pytest_bdd_before_step( - request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, - step_func_args=kwargs) - # Execute the step - step_func(**kwargs) - request.config.hook.pytest_bdd_after_step( - request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, - step_func_args=kwargs) - except Exception as exception: - request.config.hook.pytest_bdd_step_error( - request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, - step_func_args=kwargs, exception=exception) - raise + _execute_step_function(request, feature, step, step_func, example=example) -def scenario(feature_name, scenario_name, encoding='utf-8'): - """Scenario. May be called both as decorator and as just normal function.""" +FakeRequest = collections.namedtuple('FakeRequest', ['module']) - caller_module = get_caller_module() - caller_function = get_caller_function() - def decorator(request): +def get_fixture(caller_module, fixture, path=None, module=None): + """Get first conftest module from given one.""" + def call_fixture(function): + args = [] + if 'request' in inspect.getargspec(function).args: + args = [FakeRequest(module=caller_module)] + return function(*args) - 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, encoding=encoding) + if not module: + module = caller_module - # 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 hasattr(module, fixture): + return call_fixture(getattr(module, fixture)) - if scenario.examples: - _execute_scenario_outline(feature, scenario, request, encoding) - else: - _execute_scenario(feature, scenario, request, encoding) + if path is None: + path = os.path.dirname(module.__file__) + if os.path.exists(os.path.join(path, '__init__.py')): + file_path = os.path.join(path, 'conftest.py') + if os.path.exists(file_path): + conftest = imp.load_source('conftest', file_path) + if hasattr(conftest, fixture): + return get_fixture(caller_module, fixture, module=conftest) + else: + return get_fixture(caller_module, fixture, module=plugin) + return get_fixture(caller_module, fixture, path=os.path.dirname(path), module=module) - _scenario.pytestbdd_params = set() - if isinstance(request, python.FixtureRequest): - # Called as a normal function. - _scenario = recreate_function(_scenario, module=caller_module) - return _scenario(request) +def scenario( + feature_name, scenario_name, encoding='utf-8', example_converters=None, + caller_module=None, caller_function=None): + """Scenario.""" - # 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, module=caller_module) - _scenario.pytestbdd_params = set(func_args) + caller_module = caller_module or get_caller_module() + caller_function = caller_function or get_caller_function() - return _scenario + # Get the feature + base_path = get_fixture(caller_module, 'pytestbdd_feature_base_dir') + feature_path = op.abspath(op.join(base_path, feature_name)) + feature = Feature.get_feature(feature_path, encoding=encoding) - decorator = recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) + # 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 decorator + _validate_scenario(feature, scenario) + + if scenario.examples: + params = [] + for example in scenario.examples: + for index, param in enumerate(scenario.example_params): + if example_converters and param in example_converters: + example[index] = example_converters[param](example[index]) + params.append(example) + params = [scenario.example_params, params] + else: + params = [] + + g = globals().copy() + g.update(locals()) + + code = """def _scenario(request, {0}): + _execute_scenario(feature, scenario, request, encoding)""".format(','.join(scenario.example_params)) + + execute(code, g) + + _scenario = recreate_function( + g['_scenario'], module=caller_module, firstlineno=caller_function.f_lineno) + if params: + _scenario = pytest.mark.parametrize(*params)(_scenario) + _scenario.pytestbdd_params = params + _scenario.scenario = scenario + return _scenario diff --git a/pytest_bdd/steps.py b/pytest_bdd/steps.py index e69651a..a31c7c8 100644 --- a/pytest_bdd/steps.py +++ b/pytest_bdd/steps.py @@ -51,11 +51,13 @@ class StepError(Exception): # pragma: no cover RE_TYPE = type(re.compile('')) # pragma: no cover -def given(name, fixture=None): +def given(name, fixture=None, converters=None): """Given step decorator. :param name: Given step name. :param fixture: Optional name of the fixture to reuse. + :param converters: Optional `dict` of the argument or parameter converters in form + {: }. :raises: StepError in case of wrong configuration. :note: Can't be used as a decorator when the fixture is specified. @@ -66,6 +68,7 @@ def given(name, fixture=None): module = get_caller_module() step_func = lambda request: request.getfuncargvalue(fixture) step_func.step_type = GIVEN + step_func.converters = converters step_func.__name__ = name step_func.fixture = fixture func = pytest.fixture(lambda: step_func) @@ -73,29 +76,33 @@ def given(name, fixture=None): contribute_to_module(module, remove_prefix(name), func) return _not_a_fixture_decorator - return _step_decorator(GIVEN, name) + return _step_decorator(GIVEN, name, converters=converters) -def when(name): +def when(name, converters=None): """When step decorator. :param name: Step name. + :param converters: Optional `dict` of the argument or parameter converters in form + {: }. :raises: StepError in case of wrong configuration. """ - return _step_decorator(WHEN, name) + return _step_decorator(WHEN, name, converters=converters) -def then(name): +def then(name, converters=None): """Then step decorator. :param name: Step name. + :param converters: Optional `dict` of the argument or parameter converters in form + {: }. :raises: StepError in case of wrong configuration. """ - return _step_decorator(THEN, name) + return _step_decorator(THEN, name, converters=converters) def _not_a_fixture_decorator(func): @@ -109,7 +116,7 @@ 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): +def _step_decorator(step_type, step_name, converters=None): """Step decorator for the type and the name. :param step_type: Step type (GIVEN, WHEN or THEN). @@ -141,6 +148,7 @@ def _step_decorator(step_type, step_name): step_func.__name__ = step_name step_func.step_type = step_type + step_func.converters = converters @pytest.fixture def lazy_step_func(): @@ -151,6 +159,8 @@ def _step_decorator(step_type, step_name): if pattern: lazy_step_func.pattern = pattern + if converters: + lazy_step_func.converters = converters contribute_to_module( get_caller_module(), @@ -162,7 +172,7 @@ def _step_decorator(step_type, step_name): return decorator -def recreate_function(func, module=None, name=None, add_args=(), firstlineno=None): +def recreate_function(func, module=None, name=None, add_args=[], firstlineno=None): """Recreate a function, replacing some info. :param func: Function object. @@ -188,6 +198,10 @@ def recreate_function(func, module=None, name=None, add_args=(), firstlineno=Non if PY3: argnames.insert(1, 'co_kwonlyargcount') + for arg in inspect.getargspec(func).args: + if arg in add_args: + add_args.remove(arg) + args = [] code = get_code(func) for arg in argnames: @@ -220,7 +234,6 @@ def contribute_to_module(module, name, func): """ func = recreate_function(func, module=module) - setattr(module, name, func) @@ -233,3 +246,7 @@ def get_caller_module(depth=2): def get_caller_function(depth=2): """Return caller function.""" return sys._getframe(depth) + + +def execute(code, g): + exec(code, g) diff --git a/setup.py b/setup.py index 57ea648..5e659fc 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup from setuptools.command.test import test as TestCommand -version = '1.0.0' +version = '2.0.0' class Tox(TestCommand): diff --git a/tests/args/args_steps.feature b/tests/args/args_steps.feature index 9de6211..2e51949 100644 --- a/tests/args/args_steps.feature +++ b/tests/args/args_steps.feature @@ -1,11 +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... + 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 + Given I have 1 Euro + And I have 2 Euro diff --git a/tests/args/test_args_steps.py b/tests/args/test_args_steps.py index c7c22f7..1c28516 100644 --- a/tests/args/test_args_steps.py +++ b/tests/args/test_args_steps.py @@ -19,19 +19,24 @@ test_argument_in_when_step_2 = sc('Argument in when, step 2') @pytest.fixture def values(): - return ['1', '2', '1', '0', '999999'] + return [1, 2, 1, 0, 999999] -@given(re.compile(r'I have (?P\d+) Euro')) +@given(re.compile(r'I have (?P\d+) Euro'), converters=dict(euro=int)) def i_have(euro, values): assert euro == values.pop(0) -@when(re.compile(r'I pay (?P\d+) Euro')) +@when(re.compile(r'I pay (?P\d+) Euro'), converters=dict(euro=int)) def i_pay(euro, values, request): assert euro == values.pop(0) +@then(re.compile(r'I should have (?P\d+) Euro'), converters=dict(euro=int)) +def i_should_have(euro, values): + assert euro == values.pop(0) + + @given('I have an argument') def argument(): """I have an argument.""" @@ -44,11 +49,6 @@ def get_argument(argument, arg): argument['arg'] = arg -@then(re.compile(r'I should have (?P\d+) Euro')) -def i_should_have(euro, values): - assert euro == values.pop(0) - - @then(re.compile('My argument should be (?P\d+)')) def assert_that_my_argument_is_arg(argument, arg): """Assert that arg from when equals arg.""" diff --git a/tests/feature/outline.feature b/tests/feature/outline.feature index 50a3332..947ae52 100644 --- a/tests/feature/outline.feature +++ b/tests/feature/outline.feature @@ -6,3 +6,24 @@ Scenario Outline: Outlined given, when, thens Examples: | start | eat | left | | 12 | 5 | 7 | + + +Scenario Outline: Outlined with wrong examples + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | unknown_param | + | 12 | 5 | 7 | value | + + +Scenario Outline: Outlined with some examples failing + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: + | start | eat | left | + | 0 | 5 | 5 | + | 12 | 5 | 7 | diff --git a/tests/feature/test_feature_base_dir.py b/tests/feature/test_feature_base_dir.py index e8adcc9..8fb9b51 100644 --- a/tests/feature/test_feature_base_dir.py +++ b/tests/feature/test_feature_base_dir.py @@ -20,8 +20,6 @@ def pytestbdd_feature_base_dir(): def test_feature_path(request, scenario_name): """Test feature base dir.""" - sc = scenario('steps.feature', scenario_name) with pytest.raises(IOError) as exc: - sc(request) - + scenario('steps.feature', scenario_name) assert os.path.join('/does/not/exist/', 'steps.feature') in str(exc.value) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 54d2ab9..570e9f1 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -1,25 +1,54 @@ """Scenario Outline tests.""" +import re + +import pytest + from pytest_bdd import given, when, then, scenario +from pytest_bdd import mark +from pytest_bdd.scenario import ScenarioExamplesNotValidError test_outlined = scenario( 'outline.feature', 'Outlined given, when, thens', + example_converters=dict(start=int, eat=float, left=str) ) @given('there are cucumbers') def start_cucumbers(start): - return dict(start=int(start)) + assert isinstance(start, int) + return dict(start=start) @when('I eat cucumbers') -def eat_cucumbers(start_cucumbers, start, eat): - start_cucumbers['eat'] = int(eat) +def eat_cucumbers(start_cucumbers, eat): + assert isinstance(eat, float) + start_cucumbers['eat'] = eat @then('I should have cucumbers') def should_have_left_cucumbers(start_cucumbers, start, eat, left): - assert int(start) - int(eat) == int(left) - assert start_cucumbers['start'] == int(start) - assert start_cucumbers['eat'] == int(eat) + assert isinstance(left, str) + assert start - eat == int(left) + assert start_cucumbers['start'] == start + assert start_cucumbers['eat'] == eat + + +def test_wrongly_outlined(request): + """Test parametrized scenario when the test function lacks parameters.""" + + with pytest.raises(ScenarioExamplesNotValidError) as exc: + @mark.scenario( + 'outline.feature', + 'Outlined with wrong examples', + ) + def wrongly_outlined(request): + pass + + assert re.match( + """Scenario \"Outlined with wrong examples\" in the feature \"(.+)\" has not valid examples\. """ + """Set of step parameters (.+) should match set of example values """ + """(.+)\.""", + exc.value.args[0] + ) diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index abebba4..b1af275 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -1,14 +1,12 @@ import pytest -from pytest_bdd.scenario import NotEnoughScenarioParams - -from pytest_bdd import given, when, then, scenario +from pytest_bdd import given, when, then, mark @pytest.mark.parametrize( ['start', 'eat', 'left'], [(12, 5, 7)]) -@scenario( +@mark.scenario( 'parametrized.feature', 'Parametrized given, when, thens', ) @@ -18,37 +16,18 @@ def test_parametrized(request, start, eat, left): @pytest.fixture(params=[1, 2]) def foo_bar(request): - return 'foo_bar' * request.param + return 'bar' * request.param @pytest.mark.parametrize( ['start', 'eat', 'left'], [(12, 5, 7)]) -@scenario( +@mark.scenario( 'parametrized.feature', 'Parametrized given, when, thens', ) def test_parametrized_with_other_fixtures(request, start, eat, left, foo_bar): - """Test parametrized scenario, but also with other fixtures.""" - - -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: [].""" - ) + """Test parametrized scenario, but also with other parametrized fixtures.""" @given('there are cucumbers') diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index 86ca1f6..95a7efc 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -6,10 +6,9 @@ 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) + scenario( + 'not_found.feature', + 'NOT FOUND' + ) diff --git a/tests/feature/test_steps.py b/tests/feature/test_steps.py index fd92b61..662a844 100644 --- a/tests/feature/test_steps.py +++ b/tests/feature/test_steps.py @@ -96,7 +96,7 @@ def test_step_hooks(testdir): """) testdir.makepyfile(""" import pytest - from pytest_bdd import given, when, scenario + from pytest_bdd import given, when, mark @given('I have a bar') def i_have_bar(): @@ -118,15 +118,15 @@ def test_step_hooks(testdir): def when_dependency_fails(dependency): pass - @scenario('test.feature', "When step's dependency a has failure") + @mark.scenario('test.feature', "When step's dependency a has failure") def test_when_dependency_fails(): pass - @scenario('test.feature', 'When step has hook on failure') + @mark.scenario('test.feature', 'When step has hook on failure') def test_when_fails(): pass - @scenario('test.feature', 'When step is not found') + @mark.scenario('test.feature', 'When step is not found') def test_when_not_found(): pass @@ -134,7 +134,7 @@ def test_step_hooks(testdir): def foo(): return 'foo' - @scenario('test.feature', 'When step validation error happens') + @mark.scenario('test.feature', 'When step validation error happens') def test_when_step_validation_error(): pass """) @@ -185,7 +185,7 @@ def test_step_trace(testdir): """) testdir.makepyfile(""" import pytest - from pytest_bdd import given, when, scenario + from pytest_bdd import given, when, scenario, mark @given('I have a bar') def i_have_bar(): @@ -197,7 +197,7 @@ def test_step_trace(testdir): test_when_fails_inline = scenario('test.feature', 'When step has failure') - @scenario('test.feature', 'When step has failure') + @mark.scenario('test.feature', 'When step has failure') def test_when_fails_decorated(): pass diff --git a/tests/feature/test_wrong.py b/tests/feature/test_wrong.py index 06b0f26..908c012 100644 --- a/tests/feature/test_wrong.py +++ b/tests/feature/test_wrong.py @@ -33,9 +33,8 @@ def then_nevermind(): def test_wrong(request, feature, scenario_name): """Test wrong feature scenarios.""" - sc = scenario(feature, scenario_name) with pytest.raises(FeatureError): - sc(request) + scenario(feature, scenario_name) # TODO: assert the exception args from parameters @@ -60,9 +59,8 @@ def test_wrong_type_order(request, scenario_name): def test_verbose_output(request): """Test verbose output of failed feature scenario""" - sc = scenario('when_after_then.feature', 'When after then') with pytest.raises(FeatureError) as excinfo: - sc(request) + scenario('when_after_then.feature', 'When after then') msg, line_number, line = excinfo.value.args From 6fb6fa77beadf956d4940d6a766394396838d45f Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 14 Mar 2014 09:22:15 +0100 Subject: [PATCH 04/11] add test for the outline --- tests/feature/outline.feature | 1 + tests/feature/test_outline.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/tests/feature/outline.feature b/tests/feature/outline.feature index 947ae52..07f27dc 100644 --- a/tests/feature/outline.feature +++ b/tests/feature/outline.feature @@ -6,6 +6,7 @@ Scenario Outline: Outlined given, when, thens Examples: | start | eat | left | | 12 | 5 | 7 | + | 5 | 4 | 1 | Scenario Outline: Outlined with wrong examples diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 570e9f1..f85a958 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -52,3 +52,17 @@ def test_wrongly_outlined(request): """(.+)\.""", exc.value.args[0] ) + + +@pytest.fixture(params=[1, 2, 3]) +def other_fixture(request): + return request.param + + +@mark.scenario( + 'outline.feature', + 'Outlined given, when, thens', + example_converters=dict(start=int, eat=float, left=str) +) +def test_outlined_with_other_fixtures(start, eat, left, other_fixture): + """Test outlined scenario also using other parametrized fixture.""" From 2a43453a4008f208f3c9319c1254a3d99204f14e Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 14 Mar 2014 09:27:26 +0100 Subject: [PATCH 05/11] save function prototype --- pytest_bdd/mark.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pytest_bdd/mark.py b/pytest_bdd/mark.py index 99b8d84..498ac82 100644 --- a/pytest_bdd/mark.py +++ b/pytest_bdd/mark.py @@ -34,12 +34,15 @@ def scenario(feature_name, scenario_name, encoding='utf-8', example_converters=N if 'request' not in sc_args: sc_args.insert(0, 'request') - code = """def _decorated_scenario({0}): - _scenario({1})""".format(', '.join(args), ', '.join(sc_args)) + code = """def {name}({args}): + _scenario({scenario_args})""".format( + name=request.__name__, + args=', '.join(args), + scenario_args=', '.join(sc_args)) execute(code, g) - _scenario = recreate_function(g['_decorated_scenario'], module=caller_module, add_args=args) + _scenario = recreate_function(g[request.__name__], module=caller_module, add_args=args) if pytestbdd_params: _scenario = pytest.mark.parametrize(*pytestbdd_params)(_scenario) From e23d8e61800e3f34431eea45d954153e5ab239bf Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 14 Mar 2014 09:54:17 +0100 Subject: [PATCH 06/11] update the prototype of decorated function --- pytest_bdd/mark.py | 6 +++++- tests/feature/test_outline.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pytest_bdd/mark.py b/pytest_bdd/mark.py index 498ac82..5840335 100644 --- a/pytest_bdd/mark.py +++ b/pytest_bdd/mark.py @@ -34,6 +34,10 @@ def scenario(feature_name, scenario_name, encoding='utf-8', example_converters=N if 'request' not in sc_args: sc_args.insert(0, 'request') + for arg in scenario.example_params: + if arg not in args: + args.append(arg) + code = """def {name}({args}): _scenario({scenario_args})""".format( name=request.__name__, @@ -42,7 +46,7 @@ def scenario(feature_name, scenario_name, encoding='utf-8', example_converters=N execute(code, g) - _scenario = recreate_function(g[request.__name__], module=caller_module, add_args=args) + _scenario = recreate_function(g[request.__name__], module=caller_module) if pytestbdd_params: _scenario = pytest.mark.parametrize(*pytestbdd_params)(_scenario) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index f85a958..718d9e1 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -64,5 +64,5 @@ def other_fixture(request): 'Outlined given, when, thens', example_converters=dict(start=int, eat=float, left=str) ) -def test_outlined_with_other_fixtures(start, eat, left, other_fixture): +def test_outlined_with_other_fixtures(other_fixture): """Test outlined scenario also using other parametrized fixture.""" From c5237a217ead0259b5a3f685401e17599a3b17e0 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 14 Mar 2014 13:20:43 +0100 Subject: [PATCH 07/11] move to single decorator approach --- pytest_bdd/__init__.py | 3 +- pytest_bdd/mark.py | 56 ------------------------------ pytest_bdd/scenario.py | 48 +++++++++++++++++++------ pytest_bdd/scripts.py | 23 ++++++++++++ setup.py | 3 ++ tests/args/subfolder/test_args.py | 4 ++- tests/args/test_arg_fixture_mix.py | 18 +++++----- tests/args/test_args_steps.py | 8 +++-- tests/feature/test_alias.py | 4 ++- tests/feature/test_outline.py | 11 +++--- tests/feature/test_parametrized.py | 6 ++-- tests/feature/test_reuse.py | 5 ++- tests/feature/test_steps.py | 34 +++++++++++------- tests/feature/test_wrong.py | 12 +++++-- 14 files changed, 130 insertions(+), 105 deletions(-) delete mode 100644 pytest_bdd/mark.py create mode 100644 pytest_bdd/scripts.py diff --git a/pytest_bdd/__init__.py b/pytest_bdd/__init__.py index 11f7b49..36a63ed 100644 --- a/pytest_bdd/__init__.py +++ b/pytest_bdd/__init__.py @@ -1,5 +1,4 @@ from pytest_bdd.steps import given, when, then # pragma: no cover from pytest_bdd.scenario import scenario # pragma: no cover -from pytest_bdd import mark # pragma: no cover -__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, mark.__name__] # pragma: no cover +__all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__] # pragma: no cover diff --git a/pytest_bdd/mark.py b/pytest_bdd/mark.py deleted file mode 100644 index 5840335..0000000 --- a/pytest_bdd/mark.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Pytest-bdd markers.""" -import inspect - -import pytest - -from pytest_bdd.steps import execute, recreate_function, get_caller_module, get_caller_function - -from pytest_bdd import scenario as bdd_scenario - - -def scenario(feature_name, scenario_name, encoding='utf-8', example_converters=None): - """Scenario. May be called both as decorator and as just normal function.""" - - caller_module = get_caller_module() - caller_function = get_caller_function() - - def decorator(request): - _scenario = bdd_scenario( - feature_name, scenario_name, encoding=encoding, example_converters=example_converters, - caller_module=caller_module, caller_function=caller_function) - - args = inspect.getargspec(request).args - - if 'request' not in args: - args.insert(0, 'request') - - g = globals().copy() - g.update(locals()) - - pytestbdd_params = _scenario.pytestbdd_params - scenario = _scenario.scenario - - sc_args = list(scenario.example_params) - if 'request' not in sc_args: - sc_args.insert(0, 'request') - - for arg in scenario.example_params: - if arg not in args: - args.append(arg) - - code = """def {name}({args}): - _scenario({scenario_args})""".format( - name=request.__name__, - args=', '.join(args), - scenario_args=', '.join(sc_args)) - - execute(code, g) - - _scenario = recreate_function(g[request.__name__], module=caller_module) - - if pytestbdd_params: - _scenario = pytest.mark.parametrize(*pytestbdd_params)(_scenario) - - return _scenario - - return recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 3279d2f..2b6b76c 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -30,6 +30,10 @@ from pytest_bdd.types import GIVEN from pytest_bdd import plugin +class ScenarioIsDecoratorOnly(Exception): + """Scenario can be only used as decorator.""" + + class ScenarioValidationError(Exception): """Base class for scenario validation.""" @@ -245,6 +249,7 @@ def scenario( 'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name) ) + # Validate the scenario _validate_scenario(feature, scenario) if scenario.examples: @@ -261,15 +266,38 @@ def scenario( g = globals().copy() g.update(locals()) - code = """def _scenario(request, {0}): - _execute_scenario(feature, scenario, request, encoding)""".format(','.join(scenario.example_params)) + def decorator(_pytestbdd_function): + if isinstance(_pytestbdd_function, python.FixtureRequest): + raise ScenarioIsDecoratorOnly( + 'scenario function can only be used as a decorator. Refer to the documentation.') - execute(code, g) + g.update(locals()) - _scenario = recreate_function( - g['_scenario'], module=caller_module, firstlineno=caller_function.f_lineno) - if params: - _scenario = pytest.mark.parametrize(*params)(_scenario) - _scenario.pytestbdd_params = params - _scenario.scenario = scenario - return _scenario + args = inspect.getargspec(_pytestbdd_function).args + function_args = list(args) + for arg in scenario.example_params: + if arg not in function_args: + function_args.append(arg) + if 'request' not in function_args: + function_args.append('request') + + code = """def {name}({function_args}): + _execute_scenario(feature, scenario, request, encoding) + _pytestbdd_function({args})""".format( + name=_pytestbdd_function.__name__, + function_args=', '.join(function_args), + args=', '.join(args)) + + execute(code, g) + + _scenario = recreate_function( + g[_pytestbdd_function.__name__], module=caller_module, firstlineno=caller_function.f_lineno) + + if params: + _scenario = pytest.mark.parametrize(*params)(_scenario) + + return _scenario + + decorator = recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) + + return decorator diff --git a/pytest_bdd/scripts.py b/pytest_bdd/scripts.py new file mode 100644 index 0000000..d0aa328 --- /dev/null +++ b/pytest_bdd/scripts.py @@ -0,0 +1,23 @@ +"""pytest-bdd scripts.""" +import glob +import os.path +import re +import sys + + +MIGRATE_REGEX = re.compile(r'(\w+)\s\=\sscenario\((.+)\)') + + +def migrate_tests(): + """Migrate outdated tests to the most recent form.""" + if len(sys.argv) != 2: + print 'Usage: pytestbdd_migrate_tests ' + sys.exit(1) + path = sys.argv[1] + for file_path in glob.iglob(os.path.join(os.path.abspath(path), '**', '*.py')): + migrate_tests_in_file(file_path) + + +def migrate_tests_in_file(file_path): + """Migrate all bdd-based tests in the given test file.""" + re.sub(MIGRATE_REGEX, '@scenario(2)\ndef 1():\n pass', open(file_path), flags=re.MULTILINE) diff --git a/setup.py b/setup.py index 5e659fc..1ef5b1d 100755 --- a/setup.py +++ b/setup.py @@ -59,6 +59,9 @@ setup( entry_points={ 'pytest11': [ 'pytest-bdd = pytest_bdd.plugin', + ], + 'console_scripts': [ + 'pytestbdd_migrate_tests = pytest_bdd.scripts:migrate_tests' ] }, tests_require=['detox'], diff --git a/tests/args/subfolder/test_args.py b/tests/args/subfolder/test_args.py index 364bd00..0ef5e80 100644 --- a/tests/args/subfolder/test_args.py +++ b/tests/args/subfolder/test_args.py @@ -1,10 +1,12 @@ from pytest_bdd import scenario, given, then -test_steps = scenario( +@scenario( 'args.feature', 'Executed with steps matching step definitons with arguments', ) +def test_steps(): + pass @given('I have a foo fixture with value "foo"') diff --git a/tests/args/test_arg_fixture_mix.py b/tests/args/test_arg_fixture_mix.py index d299928..d31454f 100644 --- a/tests/args/test_arg_fixture_mix.py +++ b/tests/args/test_arg_fixture_mix.py @@ -14,12 +14,12 @@ def test_arg_fixture_mix(testdir): def foo(): return "fine" - - test_args = scenario( + @scenario( 'arg_and_fixture_mix.feature', 'Use the step argument with the same name as fixture of another test', ) - + def test_args(): + pass @given(re.compile(r'foo is "(?P\w+)"')) def foo1(foo): @@ -30,12 +30,12 @@ def test_arg_fixture_mix(testdir): def foo_should_be(foo, foo_value): assert foo == foo_value - - test_bar = scenario( + @scenario( 'arg_and_fixture_mix.feature', 'Everything is fine', ) - + def test_bar(): + pass @given(re.compile(r'it is all fine')) def fine(): @@ -52,12 +52,12 @@ def test_arg_fixture_mix(testdir): import pytest from pytest_bdd import scenario, given, then - - test_args = scenario( + @scenario( 'arg_and_fixture_mix.feature', 'Everything is fine', ) - + def test_args(): + pass @pytest.fixture def foo(): diff --git a/tests/args/test_args_steps.py b/tests/args/test_args_steps.py index 1c28516..8de6b0b 100644 --- a/tests/args/test_args_steps.py +++ b/tests/args/test_args_steps.py @@ -7,10 +7,12 @@ from pytest_bdd import scenario, given, when, then from pytest_bdd.scenario import GivenAlreadyUsed -test_steps = scenario( +@scenario( 'args_steps.feature', 'Every step takes a parameter with the same name', ) +def test_steps(): + pass sc = functools.partial(scenario, 'when_arguments.feature') test_argument_in_when_step_1 = sc('Argument in when, step 1') @@ -57,9 +59,11 @@ def assert_that_my_argument_is_arg(argument, arg): def test_multiple_given(request): """Using the same given fixture raises an error.""" - test = scenario( + @scenario( 'args_steps.feature', 'Using the same given fixture raises an error', ) + def test(): + pass with pytest.raises(GivenAlreadyUsed): test(request) diff --git a/tests/feature/test_alias.py b/tests/feature/test_alias.py index eb29611..ec5df0d 100644 --- a/tests/feature/test_alias.py +++ b/tests/feature/test_alias.py @@ -3,7 +3,9 @@ from pytest_bdd import scenario, given, when, then -test_steps = scenario('alias.feature', 'Multiple given alias is not evaluated multiple times') +@scenario('alias.feature', 'Multiple given alias is not evaluated multiple times') +def test_steps(): + pass @given('I have an empty list') diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index 718d9e1..e60bce4 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -4,15 +4,16 @@ import re import pytest from pytest_bdd import given, when, then, scenario -from pytest_bdd import mark from pytest_bdd.scenario import ScenarioExamplesNotValidError -test_outlined = scenario( +@scenario( 'outline.feature', 'Outlined given, when, thens', example_converters=dict(start=int, eat=float, left=str) ) +def test_outlined(): + assert 1 @given('there are cucumbers') @@ -39,11 +40,11 @@ def test_wrongly_outlined(request): """Test parametrized scenario when the test function lacks parameters.""" with pytest.raises(ScenarioExamplesNotValidError) as exc: - @mark.scenario( + @scenario( 'outline.feature', 'Outlined with wrong examples', ) - def wrongly_outlined(request): + def wrongly_outlined(): pass assert re.match( @@ -59,7 +60,7 @@ def other_fixture(request): return request.param -@mark.scenario( +@scenario( 'outline.feature', 'Outlined given, when, thens', example_converters=dict(start=int, eat=float, left=str) diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index b1af275..7dfd76d 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -1,12 +1,12 @@ import pytest -from pytest_bdd import given, when, then, mark +from pytest_bdd import given, when, then, scenario @pytest.mark.parametrize( ['start', 'eat', 'left'], [(12, 5, 7)]) -@mark.scenario( +@scenario( 'parametrized.feature', 'Parametrized given, when, thens', ) @@ -22,7 +22,7 @@ def foo_bar(request): @pytest.mark.parametrize( ['start', 'eat', 'left'], [(12, 5, 7)]) -@mark.scenario( +@scenario( 'parametrized.feature', 'Parametrized given, when, thens', ) diff --git a/tests/feature/test_reuse.py b/tests/feature/test_reuse.py index 9131faa..27828a1 100644 --- a/tests/feature/test_reuse.py +++ b/tests/feature/test_reuse.py @@ -2,10 +2,13 @@ from pytest_bdd.steps import when from pytest_bdd import given, then, scenario -test_reuse = scenario( + +@scenario( 'reuse.feature', 'Given and when using the same fixture should not evaluate it twice', ) +def test_reuse(): + pass @given('I have an empty list') diff --git a/tests/feature/test_steps.py b/tests/feature/test_steps.py index 662a844..3ac346a 100644 --- a/tests/feature/test_steps.py +++ b/tests/feature/test_steps.py @@ -4,7 +4,9 @@ from pytest_bdd import scenario, given, when, then from pytest_bdd.scenario import GivenAlreadyUsed -test_steps = scenario('steps.feature', 'Executed step by step') +@scenario('steps.feature', 'Executed step by step') +def test_steps(): + pass @given('I have a foo fixture with value "foo"') @@ -42,7 +44,9 @@ def check_results(results): assert results == [1, 2, 3] -test_when_first = scenario('steps.feature', 'When step can be the first') +@scenario('steps.feature', 'When step can be the first') +def test_when_first(): + pass @when('I do nothing') @@ -96,7 +100,7 @@ def test_step_hooks(testdir): """) testdir.makepyfile(""" import pytest - from pytest_bdd import given, when, mark + from pytest_bdd import given, when, scenario @given('I have a bar') def i_have_bar(): @@ -118,15 +122,15 @@ def test_step_hooks(testdir): def when_dependency_fails(dependency): pass - @mark.scenario('test.feature', "When step's dependency a has failure") + @scenario('test.feature', "When step's dependency a has failure") def test_when_dependency_fails(): pass - @mark.scenario('test.feature', 'When step has hook on failure') + @scenario('test.feature', 'When step has hook on failure') def test_when_fails(): pass - @mark.scenario('test.feature', 'When step is not found') + @scenario('test.feature', 'When step is not found') def test_when_not_found(): pass @@ -134,7 +138,7 @@ def test_step_hooks(testdir): def foo(): return 'foo' - @mark.scenario('test.feature', 'When step validation error happens') + @scenario('test.feature', 'When step validation error happens') def test_when_step_validation_error(): pass """) @@ -185,7 +189,7 @@ def test_step_trace(testdir): """) testdir.makepyfile(""" import pytest - from pytest_bdd import given, when, scenario, mark + from pytest_bdd import given, when, scenario @given('I have a bar') def i_have_bar(): @@ -195,19 +199,25 @@ def test_step_trace(testdir): def when_it_fails(): raise Exception('when fails') - test_when_fails_inline = scenario('test.feature', 'When step has failure') + @scenario('test.feature', 'When step has failure') + def test_when_fails_inline(): + pass - @mark.scenario('test.feature', 'When step has failure') + @scenario('test.feature', 'When step has failure') def test_when_fails_decorated(): pass - test_when_not_found = scenario('test.feature', 'When step is not found') + @scenario('test.feature', 'When step is not found') + def test_when_not_found(): + pass @when('foo') def foo(): return 'foo' - test_when_step_validation_error = scenario('test.feature', 'When step validation error happens') + @scenario('test.feature', 'When step validation error happens') + def test_when_step_validation_error(): + pass """) result = testdir.runpytest('-k test_when_fails_inline', '-vv') assert result.ret == 1 diff --git a/tests/feature/test_wrong.py b/tests/feature/test_wrong.py index 908c012..0edf901 100644 --- a/tests/feature/test_wrong.py +++ b/tests/feature/test_wrong.py @@ -34,7 +34,9 @@ def test_wrong(request, feature, scenario_name): """Test wrong feature scenarios.""" with pytest.raises(FeatureError): - scenario(feature, scenario_name) + @scenario(feature, scenario_name) + def test_scenario(): + pass # TODO: assert the exception args from parameters @@ -51,9 +53,13 @@ def test_wrong(request, feature, scenario_name): ) def test_wrong_type_order(request, scenario_name): """Test wrong step type order.""" - sc = scenario('wrong_type_order.feature', scenario_name) + @scenario('wrong_type_order.feature', scenario_name) + def test_wrong_type_order(request): + pass + with pytest.raises(StepTypeError) as excinfo: - sc(request) + test_wrong_type_order(request) + excinfo # TODO: assert the exception args from parameters From d052bbe998854979ecd158c083a70c94086c84af Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 14 Mar 2014 13:36:36 +0100 Subject: [PATCH 08/11] migrate --- .gitignore | 1 + pytest_bdd/scripts.py | 12 ++++++++---- setup.py | 5 ++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9187fa7..054d99c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ nosetests.xml /include /src /share +/local diff --git a/pytest_bdd/scripts.py b/pytest_bdd/scripts.py index d0aa328..b870d33 100644 --- a/pytest_bdd/scripts.py +++ b/pytest_bdd/scripts.py @@ -1,11 +1,11 @@ """pytest-bdd scripts.""" -import glob +import glob2 import os.path import re import sys -MIGRATE_REGEX = re.compile(r'(\w+)\s\=\sscenario\((.+)\)') +MIGRATE_REGEX = re.compile(r'\s?(\w+)\s\=\sscenario\((.+)\)', flags=re.MULTILINE) def migrate_tests(): @@ -14,10 +14,14 @@ def migrate_tests(): print 'Usage: pytestbdd_migrate_tests ' sys.exit(1) path = sys.argv[1] - for file_path in glob.iglob(os.path.join(os.path.abspath(path), '**', '*.py')): + for file_path in glob2.iglob(os.path.join(os.path.abspath(path), '**', '*.py')): migrate_tests_in_file(file_path) def migrate_tests_in_file(file_path): """Migrate all bdd-based tests in the given test file.""" - re.sub(MIGRATE_REGEX, '@scenario(2)\ndef 1():\n pass', open(file_path), flags=re.MULTILINE) + with open(file_path, 'w') as fd: + content = fd.read() + content = MIGRATE_REGEX.sub('@scenario(2)\ndef 1():\n pass', content) + fd.seek(0) + fd.write(content) diff --git a/setup.py b/setup.py index 1ef5b1d..c16c417 100755 --- a/setup.py +++ b/setup.py @@ -61,9 +61,12 @@ setup( 'pytest-bdd = pytest_bdd.plugin', ], 'console_scripts': [ - 'pytestbdd_migrate_tests = pytest_bdd.scripts:migrate_tests' + 'pytestbdd_migrate_tests = pytest_bdd.scripts:migrate_tests [migrate]' ] }, tests_require=['detox'], + extras_require={ + 'migrate': ['glob2'] + }, packages=['pytest_bdd'], ) From c0ae33a241b02c6d0ffc58d3894770b426bc6f35 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Fri, 14 Mar 2014 14:15:04 +0100 Subject: [PATCH 09/11] move to single decorator --- pytest_bdd/scenario.py | 2 ++ pytest_bdd/scripts.py | 17 ++++++++++++----- tests/args/test_args_steps.py | 12 ++++++++++-- tests/feature/test_steps.py | 14 +++++++++++--- tests/steps/test_given.py | 10 ++++++++-- tests/steps/test_unicode.py | 13 +++++++++++-- 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 2b6b76c..864a16f 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -296,6 +296,8 @@ def scenario( if params: _scenario = pytest.mark.parametrize(*params)(_scenario) + _scenario.__doc__ = '{feature_name}: {scenario_name}'.format( + feature_name=feature_name, scenario_name=scenario_name) return _scenario decorator = recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) diff --git a/pytest_bdd/scripts.py b/pytest_bdd/scripts.py index b870d33..2f72c44 100644 --- a/pytest_bdd/scripts.py +++ b/pytest_bdd/scripts.py @@ -20,8 +20,15 @@ def migrate_tests(): def migrate_tests_in_file(file_path): """Migrate all bdd-based tests in the given test file.""" - with open(file_path, 'w') as fd: - content = fd.read() - content = MIGRATE_REGEX.sub('@scenario(2)\ndef 1():\n pass', content) - fd.seek(0) - fd.write(content) + try: + with open(file_path, 'r+') as fd: + content = fd.read() + new_content = MIGRATE_REGEX.sub(r'\n\n@scenario(\2)\ndef \1():\n pass\n', content) + if new_content != content: + fd.seek(0) + fd.write(new_content) + print('migrated: {0}'.format(file_path)) + else: + print('skipped: {0}'.format(file_path)) + except IOError: + pass diff --git a/tests/args/test_args_steps.py b/tests/args/test_args_steps.py index 8de6b0b..4f1ab04 100644 --- a/tests/args/test_args_steps.py +++ b/tests/args/test_args_steps.py @@ -15,8 +15,16 @@ def test_steps(): pass sc = functools.partial(scenario, 'when_arguments.feature') -test_argument_in_when_step_1 = sc('Argument in when, step 1') -test_argument_in_when_step_2 = sc('Argument in when, step 2') + + +@sc('Argument in when, step 1') +def test_argument_in_when_step_1(): + pass + + +@sc('Argument in when, step 2') +def test_argument_in_when_step_2(): + pass @pytest.fixture diff --git a/tests/feature/test_steps.py b/tests/feature/test_steps.py index 3ac346a..3ac287f 100644 --- a/tests/feature/test_steps.py +++ b/tests/feature/test_steps.py @@ -59,7 +59,9 @@ def no_errors(): assert True -test_then_after_given = scenario('steps.feature', 'Then step can follow Given step') +@scenario('steps.feature', 'Then step can follow Given step') +def test_then_after_given(): + pass @given('xyz') @@ -67,15 +69,21 @@ def xyz(): """Used in the test_same_step_name.""" return -test_conftest = scenario('steps.feature', 'All steps are declared in the conftest') + +@scenario('steps.feature', 'All steps are declared in the conftest') +def test_conftest(): + pass def test_multiple_given(request): """Using the same given fixture raises an error.""" - test = scenario( + @scenario( 'steps.feature', 'Using the same given fixture raises an error', ) + def test(): + pass + with pytest.raises(GivenAlreadyUsed): test(request) diff --git a/tests/steps/test_given.py b/tests/steps/test_given.py index 8f23b90..87b767c 100644 --- a/tests/steps/test_given.py +++ b/tests/steps/test_given.py @@ -11,9 +11,15 @@ def foo(): given('I have alias for foo', fixture='foo') given('I have an alias to the root fixture', fixture='root') -test_given_with_fixture = scenario('given.feature', 'Test reusing local fixture') -test_root_alias = scenario('given.feature', 'Test reusing root fixture') +@scenario('given.feature', 'Test reusing local fixture') +def test_given_with_fixture(): + pass + + +@scenario('given.feature', 'Test reusing root fixture') +def test_root_alias(): + pass @then('foo should be "foo"') diff --git a/tests/steps/test_unicode.py b/tests/steps/test_unicode.py index bb24e5b..278c5f9 100644 --- a/tests/steps/test_unicode.py +++ b/tests/steps/test_unicode.py @@ -8,8 +8,17 @@ import functools from pytest_bdd import scenario, given, then scenario = functools.partial(scenario, 'unicode.feature') -test_steps_in_feature_file_have_unicode = scenario('Steps in .feature file have unicode') -test_steps_in_py_file_have_unicode = scenario('Steps in .py file have unicode') + + +@scenario('Steps in .feature file have unicode') +def test_steps_in_feature_file_have_unicode(): + pass + + +@scenario('Steps in .py file have unicode') +def test_steps_in_py_file_have_unicode(): + pass + pattern = '(?P\'\w+\')' From 482482b0599b4ef946236782061e757ae5419c46 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sat, 15 Mar 2014 01:05:21 +0100 Subject: [PATCH 10/11] code cleanup. vertical example table implemented --- CHANGES.rst | 4 +- README.rst | 91 ++++++++++++++++++------ pytest_bdd/exceptions.py | 25 +++++++ pytest_bdd/feature.py | 122 ++++++++++++++++++++++++--------- pytest_bdd/scenario.py | 120 ++++++++++++-------------------- pytest_bdd/types.py | 2 + tests/args/test_args_steps.py | 4 +- tests/feature/outline.feature | 11 +++ tests/feature/test_outline.py | 18 ++++- tests/feature/test_scenario.py | 5 +- tests/feature/test_steps.py | 4 +- tests/feature/test_wrong.py | 9 +-- 12 files changed, 270 insertions(+), 145 deletions(-) create mode 100644 pytest_bdd/exceptions.py diff --git a/CHANGES.rst b/CHANGES.rst index a214d99..829241c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,9 @@ Changelog ----- - Pure pytest parametrization for scenario outlines (bubenkoff) -- Splitting scenario decorated and non-decorated variants (bubenkoff) +- Argumented steps now support converters (transformations) (bubenkoff) +- scenario supports only decorator form (bubenkoff) +- Code generation refactoring and cleanup (bubenkoff) 1.0.0 diff --git a/README.rst b/README.rst index a14903a..d1a4ec3 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ containing the side effects of the Gherkin imperative declarations. Install pytest-bdd -================== +------------------ :: @@ -31,7 +31,7 @@ Install pytest-bdd Example -======= +------- publish\_article.feature: @@ -54,7 +54,9 @@ test\_publish\_article.py: from pytest_bdd import scenario, given, when, then - test_publish = scenario('publish_article.feature', 'Publishing the article') + @scenario('publish_article.feature', 'Publishing the article') + def test_publish(): + pass @given('I have an article') @@ -85,7 +87,7 @@ test\_publish\_article.py: 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 @@ -118,7 +120,7 @@ default author. 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. @@ -145,7 +147,11 @@ The code will look like: import re from pytest_bdd import scenario, given, when, then - test_arguments = scenario('arguments.feature', 'Arguments for given, when, thens') + + @scenario('arguments.feature', 'Arguments for given, when, thens') + def test_arguments(): + pass + @given(re.compile('there are (?P\d+) cucumbers'), converters=dict(start=int)) def start_cucumbers(start): @@ -167,15 +173,15 @@ different than strings. Scenario parameters -=================== -Scenario function/decorator can accept such optional keyword arguments: +------------------- +Scenario decorator can accept such optional keyword arguments: * `encoding` - decode content of feature file in specific encoding. UTF-8 is default. * `example_converters` - mapping to pass functions to convert example values provided in feature files. Scenario outlines -================= +----------------- Scenarios can be parametrized to cover few cases. In Gherkin the variable templates are written using corner braces as . @@ -195,6 +201,24 @@ Example: | start | eat | left | | 12 | 5 | 7 | +pytest-bdd feature file format also supports example tables in different way: + + +.. code-block:: feature + + Scenario Outline: Outlined given, when, thens + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: Vertical + | start | 12 | 2 | + | eat | 5 | 1 | + | left | 7 | 1 | + +This form allows to have tables with lots of columns keeping the maximum text width predictable without significant +readability change. + The code will look like: @@ -203,11 +227,13 @@ The code will look like: from pytest_bdd import given, when, then, scenario - test_outlined = scenario( + @scenario( 'outline.feature', 'Outlined given, when, thens', example_converters=dict(start=int, eat=float, left=str) ) + def test_outlined(): + pass @given('there are cucumbers') @@ -242,6 +268,7 @@ The code will look like: import pytest from pytest_bdd import mark, given, when, then + # Here we use pytest to parametrize the test with the parameters table @pytest.mark.parametrize( ['start', 'eat', 'left'], @@ -276,7 +303,7 @@ The significant downside of this approach is inability to see the test table fro 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 @@ -366,7 +393,7 @@ Will raise an exception if the step is using the regular expression pattern. Reusing fixtures -================ +---------------- Sometimes scenarios define new names for the fixture that can be inherited. Fixtures can be reused with other names using given(): @@ -377,7 +404,7 @@ inherited. Fixtures can be reused with other names using given(): Reusing steps -============= +------------- It is possible to define some common steps in the parent conftest.py and simply expect them in the child test file. @@ -410,14 +437,16 @@ test\_common.py: .. code-block:: python - test_conftest = scenario('common_steps.feature', 'All steps are declared in the conftest') + @scenario('common_steps.feature', 'All steps are declared in the conftest') + def test_conftest(): + pass 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 @@ -436,11 +465,14 @@ test\_publish\_article.py: def pytestbdd_feature_base_dir(): return '/home/user/projects/foo.bar/features' - test_publish = scenario('publish_article.feature', 'Publishing the article') + + @scenario('publish_article.feature', 'Publishing the article') + def test_publish(): + pass 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. @@ -459,15 +491,22 @@ test\_publish\_article.py: 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') + + @scenario('Publishing the article') + def test_publish(): + pass + + + @scenario('Publishing the article as unprivileged user') + def test_publish_unprivileged(): + pass You can learn more about `functools.partial `_ in the Python docs. Hooks -===== +----- pytest-bdd exposes several pytest `hooks `_ which might be helpful building useful reporting, visualization, etc on top of it: @@ -488,7 +527,15 @@ which might be helpful building useful reporting, visualization, etc on top of i Browser testing -=============== +--------------- + +Tools recommended to use for browser testing: + + * pytest-splinter - pytest splinter integration for the real browser testing + + +Migration of your tests from versions 0.x.x-1.x.x +------------------------------------------------- Tools recommended to use for browser testing: @@ -496,7 +543,7 @@ Tools recommended to use for browser testing: License -======= +------- This software is licensed under the `MIT license `_. diff --git a/pytest_bdd/exceptions.py b/pytest_bdd/exceptions.py new file mode 100644 index 0000000..e59febb --- /dev/null +++ b/pytest_bdd/exceptions.py @@ -0,0 +1,25 @@ +"""pytest-bdd Exceptions.""" + + +class ScenarioIsDecoratorOnly(Exception): + """Scenario can be only used as decorator.""" + + +class ScenarioValidationError(Exception): + """Base class for scenario validation.""" + + +class ScenarioNotFound(ScenarioValidationError): # pragma: no cover + """Scenario Not Found""" + + +class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover + """Scenario steps argumets do not match declared scenario examples.""" + + +class StepTypeError(ScenarioValidationError): # pragma: no cover + """Step definition is not of the type expected in the scenario.""" + + +class GivenAlreadyUsed(ScenarioValidationError): # pragma: no cover + """Fixture that implements the Given has been already used.""" diff --git a/pytest_bdd/feature.py b/pytest_bdd/feature.py index b933422..786fc24 100644 --- a/pytest_bdd/feature.py +++ b/pytest_bdd/feature.py @@ -26,10 +26,8 @@ one line. import re # pragma: no cover import sys # pragma: no cover -from pytest_bdd.types import ( - FEATURE, SCENARIO, SCENARIO_OUTLINE, EXAMPLES, EXAMPLES_HEADERS, EXAMPLE_LINE, GIVEN, WHEN, - THEN # pragma: no cover -) +from pytest_bdd import types # pragma: no cover +from pytest_bdd import exceptions # pragma: no cover class FeatureError(Exception): # pragma: no cover @@ -48,16 +46,17 @@ class FeatureError(Exception): # pragma: no cover features = {} # pragma: no cover -STEP_PREFIXES = { # pragma: no cover - 'Feature: ': FEATURE, - 'Scenario Outline: ': SCENARIO_OUTLINE, - 'Examples:': EXAMPLES, - 'Scenario: ': SCENARIO, - 'Given ': GIVEN, - 'When ': WHEN, - 'Then ': THEN, - 'And ': None, # Unknown step type -} +STEP_PREFIXES = [ # pragma: no cover + ('Feature: ', types.FEATURE), + ('Scenario Outline: ', types.SCENARIO_OUTLINE), + ('Examples: Vertical', types.EXAMPLES_VERTICAL), + ('Examples:', types.EXAMPLES), + ('Scenario: ', types.SCENARIO), + ('Given ', types.GIVEN), + ('When ', types.WHEN), + ('Then ', types.THEN), + ('And ', None), # Unknown step type +] COMMENT_SYMBOLS = '#' # pragma: no cover @@ -70,9 +69,9 @@ def get_step_type(line): :param line: Line of the Feature file :return: SCENARIO, GIVEN, WHEN, THEN, or `None` if can't be detected. """ - for prefix in STEP_PREFIXES: + for prefix, _type in STEP_PREFIXES: if line.startswith(prefix): - return STEP_PREFIXES[prefix] + return _type def get_step_params(name): @@ -104,7 +103,7 @@ def remove_prefix(line): :return: Line without the prefix. """ - for prefix in STEP_PREFIXES: + for prefix, _ in STEP_PREFIXES: if line.startswith(prefix): return line[len(prefix):].strip() return line @@ -156,20 +155,21 @@ class Feature(object): continue mode = get_step_type(line) or mode - if mode == GIVEN and prev_mode not in (GIVEN, SCENARIO, SCENARIO_OUTLINE): + if mode == types.GIVEN and prev_mode not in (types.GIVEN, types.SCENARIO, types.SCENARIO_OUTLINE): raise FeatureError('Given steps must be the first in withing the Scenario', line_number, line) - if mode == WHEN and prev_mode not in (SCENARIO, SCENARIO_OUTLINE, GIVEN, WHEN): + if mode == types.WHEN and prev_mode not in ( + types.SCENARIO, types.SCENARIO_OUTLINE, types.GIVEN, types.WHEN): raise FeatureError('When steps must be the first or follow Given steps', line_number, line) - if mode == THEN and prev_mode not in (GIVEN, WHEN, THEN): + if mode == types.THEN and prev_mode not in (types.GIVEN, types.WHEN, types.THEN): raise FeatureError('Then steps must follow Given or When steps', line_number, line) - if mode == FEATURE: - if prev_mode != FEATURE: + if mode == types.FEATURE: + if prev_mode != types.FEATURE: self.name = remove_prefix(line) else: description.append(line) @@ -178,16 +178,21 @@ class Feature(object): # Remove Feature, Given, When, Then, And line = remove_prefix(line) - if mode in [SCENARIO, SCENARIO_OUTLINE]: - self.scenarios[line] = scenario = Scenario(line) - elif mode == EXAMPLES: - mode = EXAMPLES_HEADERS - elif mode == EXAMPLES_HEADERS: + if mode in [types.SCENARIO, types.SCENARIO_OUTLINE]: + self.scenarios[line] = scenario = Scenario(self, line) + elif mode == types.EXAMPLES: + mode = types.EXAMPLES_HEADERS + elif mode == types.EXAMPLES_VERTICAL: + mode = types.EXAMPLE_LINE_VERTICAL + elif mode == types.EXAMPLES_HEADERS: scenario.set_param_names([l.strip() for l in line.split('|') if l.strip()]) - mode = EXAMPLE_LINE - elif mode == EXAMPLE_LINE: + mode = types.EXAMPLE_LINE + elif mode == types.EXAMPLE_LINE: scenario.add_example([l.strip() for l in line.split('|') if l.strip()]) - elif mode and mode != FEATURE: + elif mode == types.EXAMPLE_LINE_VERTICAL: + line = [l.strip() for l in line.split('|') if l.strip()] + scenario.add_example_row(line[0], line[1:]) + elif mode and mode != types.FEATURE: scenario.add_step(step_name=line, step_type=mode) self.description = u'\n'.join(description) @@ -215,12 +220,15 @@ class Feature(object): class Scenario(object): """Scenario.""" - def __init__(self, name): + def __init__(self, feature, name, example_converters=None): + self.feature = feature self.name = name self.params = set() self.steps = [] self.example_params = [] self.examples = [] + self.vertical_examples = [] + self.example_converters = example_converters def add_step(self, step_name, step_type): """Add step to the scenario. @@ -249,6 +257,58 @@ class Scenario(object): """ self.examples.append(values) + def add_example_row(self, param, values): + """Add example row. + + :param param: `str` parameter name + :param values: `list` of `string` parameter values + + """ + if param in self.example_params: + raise exceptions.ScenarioExamplesNotValidError( + """Scenario "{0}" in the feature "{1}" has not valid examples. """ + """Example rows should contain unique parameters. {2} appeared more than once.""".format( + self.name, self.feature.filename, param, + ) + ) + self.example_params.append(param) + self.vertical_examples.append(values) + + def get_params(self): + """Get scenario pytest parametrization table.""" + param_count = len(self.example_params) + if self.vertical_examples and not self.examples: + for value_index in range(len(self.vertical_examples[0])): + example = [] + for param_index in range(param_count): + example.append(self.vertical_examples[param_index][value_index]) + self.examples.append(example) + + if self.examples: + params = [] + for example in self.examples: + for index, param in enumerate(self.example_params): + if self.example_converters and param in self.example_converters: + example[index] = self.example_converters[param](example[index]) + params.append(example) + return [self.example_params, params] + else: + return [] + + def validate(self): + """Validate the scenario. + + :raises: `ScenarioValidationError` + + """ + if self.params and self.example_params and self.params != set(self.example_params): + raise exceptions.ScenarioExamplesNotValidError( + """Scenario "{0}" in the feature "{1}" has not valid examples. """ + """Set of step parameters {2} should match set of example values {3}.""".format( + self.name, self.feature.filename, sorted(self.params), sorted(self.example_params), + ) + ) + class Step(object): """Step.""" diff --git a/pytest_bdd/scenario.py b/pytest_bdd/scenario.py index 864a16f..7102bec 100644 --- a/pytest_bdd/scenario.py +++ b/pytest_bdd/scenario.py @@ -26,34 +26,11 @@ from _pytest import python from pytest_bdd.feature import Feature, force_encode # pragma: no cover from pytest_bdd.steps import execute, recreate_function, get_caller_module, get_caller_function from pytest_bdd.types import GIVEN +from pytest_bdd import exceptions from pytest_bdd import plugin -class ScenarioIsDecoratorOnly(Exception): - """Scenario can be only used as decorator.""" - - -class ScenarioValidationError(Exception): - """Base class for scenario validation.""" - - -class ScenarioNotFound(ScenarioValidationError): # pragma: no cover - """Scenario Not Found""" - - -class ScenarioExamplesNotValidError(ScenarioValidationError): # pragma: no cover - """Scenario steps argumets do not match declared scenario examples.""" - - -class StepTypeError(ScenarioValidationError): # pragma: no cover - """Step definition is not of the type expected in the scenario.""" - - -class GivenAlreadyUsed(ScenarioValidationError): # pragma: no cover - """Fixture that implements the Given has been already used.""" - - def _inject_fixture(request, arg, value): """Inject fixture into pytest fixture request. @@ -122,17 +99,6 @@ def _find_step_function(request, name, encoding): raise -def _validate_scenario(feature, scenario): - """Validate the scenario.""" - if scenario.params and scenario.example_params and scenario.params != set(scenario.example_params): - raise ScenarioExamplesNotValidError( - """Scenario "{0}" in the feature "{1}" has not valid examples. """ - """Set of step parameters {2} should match set of example values {3}.""".format( - scenario.name, feature.filename, sorted(scenario.params), sorted(scenario.example_params), - ) - ) - - def _execute_step_function(request, feature, step, step_func, example=None): """Execute step function.""" kwargs = {} @@ -176,20 +142,20 @@ def _execute_scenario(feature, scenario, request, encoding, example=None): try: # Check the step types are called in the correct order if step_func.step_type != step.type: - raise StepTypeError( + raise exceptions.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( + raise exceptions.GivenAlreadyUsed( 'Fixture "{0}" that implements this "{1}" given step has been already used.'.format( step_func.fixture, step.name, ) ) givens.add(step_func.fixture) - except ScenarioValidationError as exception: + except exceptions.ScenarioValidationError as exception: request.config.hook.pytest_bdd_step_validation_error( request=request, feature=feature, scenario=scenario, step=step, step_func=step_func, exception=exception) @@ -228,47 +194,15 @@ def get_fixture(caller_module, fixture, path=None, module=None): return get_fixture(caller_module, fixture, path=os.path.dirname(path), module=module) -def scenario( - feature_name, scenario_name, encoding='utf-8', example_converters=None, - caller_module=None, caller_function=None): - """Scenario.""" - - caller_module = caller_module or get_caller_module() - caller_function = caller_function or get_caller_function() - - # Get the feature - base_path = get_fixture(caller_module, 'pytestbdd_feature_base_dir') - feature_path = op.abspath(op.join(base_path, feature_name)) - feature = Feature.get_feature(feature_path, encoding=encoding) - - # 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) - ) - - # Validate the scenario - _validate_scenario(feature, scenario) - - if scenario.examples: - params = [] - for example in scenario.examples: - for index, param in enumerate(scenario.example_params): - if example_converters and param in example_converters: - example[index] = example_converters[param](example[index]) - params.append(example) - params = [scenario.example_params, params] - else: - params = [] - - g = globals().copy() - g.update(locals()) +def _get_scenario_decorator( + feature, feature_name, scenario, scenario_name, caller_module, caller_function, encoding): + """Get scenario decorator.""" + g = locals() + g['_execute_scenario'] = _execute_scenario def decorator(_pytestbdd_function): if isinstance(_pytestbdd_function, python.FixtureRequest): - raise ScenarioIsDecoratorOnly( + raise exceptions.ScenarioIsDecoratorOnly( 'scenario function can only be used as a decorator. Refer to the documentation.') g.update(locals()) @@ -293,6 +227,8 @@ def scenario( _scenario = recreate_function( g[_pytestbdd_function.__name__], module=caller_module, firstlineno=caller_function.f_lineno) + params = scenario.get_params() + if params: _scenario = pytest.mark.parametrize(*params)(_scenario) @@ -300,6 +236,34 @@ def scenario( feature_name=feature_name, scenario_name=scenario_name) return _scenario - decorator = recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) + return recreate_function(decorator, module=caller_module, firstlineno=caller_function.f_lineno) - return decorator + +def scenario( + feature_name, scenario_name, encoding='utf-8', example_converters=None, + caller_module=None, caller_function=None): + """Scenario.""" + + caller_module = caller_module or get_caller_module() + caller_function = caller_function or get_caller_function() + + # Get the feature + base_path = get_fixture(caller_module, 'pytestbdd_feature_base_dir') + feature_path = op.abspath(op.join(base_path, feature_name)) + feature = Feature.get_feature(feature_path, encoding=encoding) + + # Get the scenario + try: + scenario = feature.scenarios[scenario_name] + except KeyError: + raise exceptions.ScenarioNotFound( + 'Scenario "{0}" in feature "{1}" is not found.'.format(scenario_name, feature_name) + ) + + scenario.example_converters = example_converters + + # Validate the scenario + scenario.validate() + + return _get_scenario_decorator( + feature, feature_name, scenario, scenario_name, caller_module, caller_function, encoding) diff --git a/pytest_bdd/types.py b/pytest_bdd/types.py index 8bd9f50..f8748f7 100644 --- a/pytest_bdd/types.py +++ b/pytest_bdd/types.py @@ -3,8 +3,10 @@ FEATURE = 'feature' # pragma: no cover SCENARIO_OUTLINE = 'scenario outline' # pragma: no cover EXAMPLES = 'examples' # pragma: no cover +EXAMPLES_VERTICAL = 'examples vertical' # pragma: no cover EXAMPLES_HEADERS = 'example headers' # pragma: no cover EXAMPLE_LINE = 'example line' # pragma: no cover +EXAMPLE_LINE_VERTICAL = 'example line vertical' # pragma: no cover SCENARIO = 'scenario' # pragma: no cover GIVEN = 'given' # pragma: no cover WHEN = 'when' # pragma: no cover diff --git a/tests/args/test_args_steps.py b/tests/args/test_args_steps.py index 4f1ab04..271dfd8 100644 --- a/tests/args/test_args_steps.py +++ b/tests/args/test_args_steps.py @@ -4,7 +4,7 @@ import functools import re import pytest from pytest_bdd import scenario, given, when, then -from pytest_bdd.scenario import GivenAlreadyUsed +from pytest_bdd import exceptions @scenario( @@ -73,5 +73,5 @@ def test_multiple_given(request): ) def test(): pass - with pytest.raises(GivenAlreadyUsed): + with pytest.raises(exceptions.GivenAlreadyUsed): test(request) diff --git a/tests/feature/outline.feature b/tests/feature/outline.feature index 07f27dc..b8e6368 100644 --- a/tests/feature/outline.feature +++ b/tests/feature/outline.feature @@ -28,3 +28,14 @@ Scenario Outline: Outlined with some examples failing | start | eat | left | | 0 | 5 | 5 | | 12 | 5 | 7 | + + +Scenario Outline: Outlined with vertical example table + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + + Examples: Vertical + | start | 12 | 2 | + | eat | 5 | 1 | + | left | 7 | 1 | diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index e60bce4..8bf1c19 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -4,7 +4,7 @@ import re import pytest from pytest_bdd import given, when, then, scenario -from pytest_bdd.scenario import ScenarioExamplesNotValidError +from pytest_bdd import exceptions @scenario( @@ -13,7 +13,8 @@ from pytest_bdd.scenario import ScenarioExamplesNotValidError example_converters=dict(start=int, eat=float, left=str) ) def test_outlined(): - assert 1 + assert test_outlined.parametrize.args == ( + [u'start', u'eat', u'left'], [[12, 5.0, '7'], [5, 4.0, '1']]) @given('there are cucumbers') @@ -39,7 +40,7 @@ def should_have_left_cucumbers(start_cucumbers, start, eat, left): def test_wrongly_outlined(request): """Test parametrized scenario when the test function lacks parameters.""" - with pytest.raises(ScenarioExamplesNotValidError) as exc: + with pytest.raises(exceptions.ScenarioExamplesNotValidError) as exc: @scenario( 'outline.feature', 'Outlined with wrong examples', @@ -67,3 +68,14 @@ def other_fixture(request): ) def test_outlined_with_other_fixtures(other_fixture): """Test outlined scenario also using other parametrized fixture.""" + + +@scenario( + 'outline.feature', + 'Outlined with vertical example table', + example_converters=dict(start=int, eat=float, left=str) +) +def test_vertical_example(): + """Test outlined scenario with vertical examples table.""" + assert test_vertical_example.parametrize.args == ( + [u'start', u'eat', u'left'], [[12, 5.0, '7'], [2, 1.0, '1']]) diff --git a/tests/feature/test_scenario.py b/tests/feature/test_scenario.py index 95a7efc..79e67af 100644 --- a/tests/feature/test_scenario.py +++ b/tests/feature/test_scenario.py @@ -1,13 +1,14 @@ +"""Test scenario decorator.""" import pytest from pytest_bdd import scenario -from pytest_bdd.scenario import ScenarioNotFound +from pytest_bdd import exceptions def test_scenario_not_found(request): """Test the situation when scenario is not found.""" - with pytest.raises(ScenarioNotFound): + with pytest.raises(exceptions.ScenarioNotFound): scenario( 'not_found.feature', 'NOT FOUND' diff --git a/tests/feature/test_steps.py b/tests/feature/test_steps.py index 3ac287f..0fdf726 100644 --- a/tests/feature/test_steps.py +++ b/tests/feature/test_steps.py @@ -1,7 +1,7 @@ import pytest from pytest_bdd import scenario, given, when, then -from pytest_bdd.scenario import GivenAlreadyUsed +from pytest_bdd import exceptions @scenario('steps.feature', 'Executed step by step') @@ -84,7 +84,7 @@ def test_multiple_given(request): def test(): pass - with pytest.raises(GivenAlreadyUsed): + with pytest.raises(exceptions.GivenAlreadyUsed): test(request) diff --git a/tests/feature/test_wrong.py b/tests/feature/test_wrong.py index 0edf901..ea8db09 100644 --- a/tests/feature/test_wrong.py +++ b/tests/feature/test_wrong.py @@ -1,9 +1,11 @@ """Test wrong feature syntax.""" +import re + import pytest from pytest_bdd import scenario, given, when, then from pytest_bdd.feature import FeatureError -from pytest_bdd.scenario import StepTypeError +from pytest_bdd import exceptions @given('something') @@ -57,10 +59,9 @@ def test_wrong_type_order(request, scenario_name): def test_wrong_type_order(request): pass - with pytest.raises(StepTypeError) as excinfo: + with pytest.raises(exceptions.StepTypeError) as excinfo: test_wrong_type_order(request) - - excinfo # TODO: assert the exception args from parameters + assert re.match(r'Wrong step type \"(\w+)\" while \"(\w+)\" is expected\.', excinfo.value.args[0]) def test_verbose_output(request): From 4f639ec695f5bca4a4f24e63cc1d8d35f75b6b51 Mon Sep 17 00:00:00 2001 From: Anatoly Bubenkov Date: Sat, 15 Mar 2014 01:20:37 +0100 Subject: [PATCH 11/11] migration documented --- README.rst | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d1a4ec3..c94a513 100644 --- a/README.rst +++ b/README.rst @@ -531,15 +531,45 @@ Browser testing Tools recommended to use for browser testing: - * pytest-splinter - pytest splinter integration for the real browser testing + * pytest-splinter - pytest splinter integration for the real browser testing + Migration of your tests from versions 0.x.x-1.x.x ------------------------------------------------- -Tools recommended to use for browser testing: +In version 2.0.0, the backward-incompartible change was introduced: scenario function can now only be used as a +decorator. Reasons for that: - * pytest-splinter - pytest splinter integration for the real browser testing + * test code readability is much higher using normal python function syntax; + * pytest-bdd internals are much cleaner and shorter when using single approach instead of supporting two; + * after moving to parsing-on-import-time approach for feature files, it's not possible to detect whether it's a + decorator more or not, so to support it along with functional approach there needed to be special parameter + for that, which is also a backward-incompartible change. +To help users migrate to newer version, there's migration console script provided with **migrate** extra: + + +:: + + # install extra for migration + pip install pytest-bdd[migrate] + + # run migration script + pytestbdd_migrate_tests + +Under the hood the script does the replacement from this: + +.. code-block:: python + + test_function = scenario('publish_article.feature', 'Publishing the article') + +to this: + +.. code-block:: python + + @scenario('publish_article.feature', 'Publishing the article') + def test_function(): + pass License