diff --git a/pytest_bdd/reporting.py b/pytest_bdd/reporting.py index f99d54c7..72310556 100644 --- a/pytest_bdd/reporting.py +++ b/pytest_bdd/reporting.py @@ -7,7 +7,7 @@ import time from .feature import force_unicode -from .utils import get_parametrize_markers_args +from .utils import get_parametrize_markers_args, get_parametrize_params class StepReport(object): @@ -73,20 +73,30 @@ def __init__(self, scenario, node): """ self.scenario = scenario self.step_reports = [] - self.param_index = None + parametrize_args = get_parametrize_markers_args(node) - if parametrize_args and scenario.examples: - param_names = parametrize_args[0] if isinstance(parametrize_args[0], (tuple, list)) else [ - parametrize_args[0]] - param_values = parametrize_args[1] + params = get_parametrize_params(parametrize_args) + + self.param_index = self.get_param_index(node, params) + self.example_kwargs = self.get_example_kwargs(node, params) + + def get_param_index(self, node, params): + if params: + param_names = params[0]['names'] + param_values = params[0]['values'] node_param_values = [node.funcargs[param_name] for param_name in param_names] if node_param_values in param_values: - self.param_index = param_values.index(node_param_values) + return param_values.index(node_param_values) elif tuple(node_param_values) in param_values: - self.param_index = param_values.index(tuple(node_param_values)) - self.example_kwargs = { - example_param: force_unicode(node.funcargs[example_param]) - for example_param in scenario.get_example_params() + return param_values.index(tuple(node_param_values)) + return None + + def get_example_kwargs(self, node, params): + params_names = (param['names'] for param in params) + all_names = sum(params_names, []) + return { + example_param_name: force_unicode(node.funcargs[example_param_name]) + for example_param_name in all_names } @property diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index 2c73efb2..0f5c70c6 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -49,3 +49,33 @@ def get_markers_args_using_iter_markers(node, mark_name): def get_markers_args_using_get_marker(node, mark_name): """Deprecated on pytest>=3.6""" return getattr(node.get_marker(mark_name), 'args', ()) + + +def get_parametrize_params(parametrize_args): + """Group parametrize markers arguments names and values. + + :param parametrize_args: parametrize markers arguments. + :return: `list` of `dict` in the form of: + [ + { + "names": ["name1", "name2", ...], + "values": [value1, value2, ...], + }, + ... + ] + """ + params = [] + for i in range(0, len(parametrize_args), 2): + params.append({ + 'names': _coerce_list(parametrize_args[i]), + 'values': parametrize_args[i+1] + }) + return params + + +def _coerce_list(names): + if not isinstance(names, (tuple, list)): + # As pytest.mark.parametrize has only one param name, + # it is not returned as a list. Convert it to list: + names = [names] + return list(names) diff --git a/tests/feature/gherkin_terminal_reporter.feature b/tests/feature/gherkin_terminal_reporter.feature index a7ba9f45..87ade2c5 100644 --- a/tests/feature/gherkin_terminal_reporter.feature +++ b/tests/feature/gherkin_terminal_reporter.feature @@ -45,3 +45,15 @@ Feature: Gherkin terminal reporter Given there is gherkin scenario outline implemented When I run tests with step expanded mode Then output must contain parameters values + + Scenario: Should handle unicode output in expanded mode + Given there is gherkin scenario that has unicode characters + When I run tests with step expanded mode + Then UnicodeEncodeError is not raised during test execution + And output should contain single passing test case + + Scenario: Should handle test parametrized using @pytest.mark.parametrize decorator in expanded mode + Given there is gherkin scenario that has parametrized scenario + When I run tests with step expanded mode + Then KeyError is not raised during test execution + And output should contain single passing test case diff --git a/tests/feature/outline_feature.feature b/tests/feature/outline_feature.feature index 98280b1d..992fc81f 100644 --- a/tests/feature/outline_feature.feature +++ b/tests/feature/outline_feature.feature @@ -4,6 +4,7 @@ Feature: Outline | start | eat | left | | 12 | 5 | 7 | | 5 | 4 | 1 | + | 4 | 2 | 2 | Scenario Outline: Outlined given, when, thens Given there are diff --git a/tests/feature/parametrized.feature b/tests/feature/parametrized.feature index 70953cc8..dc3779b4 100644 --- a/tests/feature/parametrized.feature +++ b/tests/feature/parametrized.feature @@ -2,3 +2,9 @@ Scenario: Parametrized given, when, thens Given there are cucumbers When I eat cucumbers Then I should have cucumbers + + +Scenario: Parametrized given, then - single parameter name + Given there are cucumbers + When I do not eat any cucumber + Then I still should have cucumbers diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index 415c2cc7..2a4e51c2 100644 --- a/tests/feature/test_gherkin_terminal_reporter.py +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -1,10 +1,8 @@ +# coding: utf-8 import re - import pytest - -from pytest_bdd import scenario, given, when, then -from tests.utils import get_test_filepath, prepare_feature_and_py_files +from pytest_bdd import given, parsers, scenario, then, when @scenario('gherkin_terminal_reporter.feature', @@ -61,6 +59,18 @@ def test_Should_step_parameters_be_replaced_by_their_values(): pass +@scenario('gherkin_terminal_reporter.feature', + 'Should handle unicode output in expanded mode') +def test_Should_handle_unicode_output_in_expanded_mode(): + pass + + +@scenario('gherkin_terminal_reporter.feature', + 'Should handle test parametrized using @pytest.mark.parametrize decorator in expanded mode') +def test_Should_handle_test_parametrized_using_pytest_mark_parametrize_decorator_in_expanded_mode(): + pass + + @pytest.fixture(params=[0, 1, 2], ids=['compact mode', 'line per test', 'verbose']) def verbosity_mode(request): @@ -152,6 +162,85 @@ def test_scenario_2(): return example +@given("there is gherkin scenario that has unicode characters") +def gherkin_scenario_that_has_unicode_characters(testdir): + testdir.makefile('.feature', test=""" + Feature: Юнікодні символи + + Scenario: Кроки в .feature файлі містять юнікод + Given у мене є рядок який містить 'якийсь контент' + Then I should see that the string equals to content 'якийсь контент' + """) + testdir.makepyfile(test_gherkin=""" + # coding: utf-8 + import functools + import sys + + import pytest + + from pytest_bdd import given, parsers, scenario, then + + scenario = functools.partial(scenario, 'test.feature') + + @scenario('Кроки в .feature файлі містять юнікод') + def test_steps_in_feature_file_have_unicode(): + pass + + @pytest.fixture + def string(): + return {'content': ''} + + @given(parsers.parse(u"у мене є рядок який містить '{content}'")) + def there_is_a_string_with_content(content, string): + string['content'] = content + + @then(parsers.parse("I should see that the string equals to content '{content}'")) + def assert_that_the_string_equals_to_content(content, string): + assert string['content'] == content + if sys.version_info < (3, 0): + assert isinstance(content, unicode) + """) + + +@given("there is gherkin scenario that has parametrized scenario") +def gherkin_scenario_that_has_parametrized_scenario(testdir): + testdir.makefile('.feature', test=""" + Scenario: Parametrized given, when, thens + Given there are cucumbers + When I eat cucumbers + Then I should have cucumbers + """) + testdir.makepyfile(test_gherkin=""" + import pytest + + from pytest_bdd import given, when, then, scenario + + @pytest.mark.parametrize( + ['start', 'eat', 'left'], + [(12, 5, 7)]) + @scenario( + 'test.feature', + 'Parametrized given, when, thens', + ) + def test_parametrized(request, start, eat, left): + pass + + @given('there are cucumbers') + def start_cucumbers(start): + return dict(start=start) + + @when('I eat cucumbers') + def eat_cucumbers(start_cucumbers, start, eat): + start_cucumbers['eat'] = eat + + @then('I should have cucumbers') + def should_have_left_cucumbers(start_cucumbers, start, eat, left): + assert start - eat == left + assert start_cucumbers['start'] == start + assert start_cucumbers['eat'] == eat + """) + + @when("I run tests") def run_tests(testdir, test_execution): test_execution['regular'] = testdir.runpytest() @@ -347,20 +436,13 @@ def output_output_must_contain_parameters_values(test_execution, gherkin_scenari ghe.stdout.fnmatch_lines('*PASSED') -@pytest.mark.parametrize( - 'feature_file, py_file, name', [ - ('./steps/unicode.feature', './steps/test_unicode.py', 'test_steps_in_feature_file_have_unicode') - ] -) -def test_scenario_in_expanded_mode(testdir, test_execution, feature_file, py_file, name): - prepare_feature_and_py_files(testdir, feature_file, py_file) +@then(parsers.parse('{error} is not raised during test execution')) +def error_is_not_raised_during_test_execution(test_execution, error): + ghe = test_execution['gherkin'] + assert error not in ghe.stdout.str() - test_execution['gherkin'] = testdir.runpytest( - '-k %s' % name, - '--gherkin-terminal-reporter', - '--gherkin-terminal-reporter-expanded', - '-vv', - ) +@then("output should contain single passing test case") +def output_must_contain_single_passing_test_case(test_execution): ghe = test_execution['gherkin'] ghe.assert_outcomes(passed=1) diff --git a/tests/feature/test_outline.py b/tests/feature/test_outline.py index afddaa81..ca6bcb15 100644 --- a/tests/feature/test_outline.py +++ b/tests/feature/test_outline.py @@ -168,7 +168,7 @@ def should_have_left_fruits(start_fruits, start, eat, left, fruits): def test_outlined_feature(request): assert get_parametrize_markers_args(request.node) == ( ['start', 'eat', 'left'], - [[12, 5.0, '7'], [5, 4.0, '1']], + [[12, 5.0, '7'], [5, 4.0, '1'], [4, 2.0, '2']], ['fruits'], [[u'oranges'], [u'apples']] ) diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index 7dfd76da..849b0aa7 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -14,6 +14,17 @@ def test_parametrized(request, start, eat, left): """Test parametrized scenario.""" +@pytest.mark.parametrize( + 'start', [12, 5] +) +@scenario( + 'parametrized.feature', + 'Parametrized given, then - single parameter name', +) +def test_parametrized_single_parameter_name(request, start): + """Test parametrized scenario.""" + + @pytest.fixture(params=[1, 2]) def foo_bar(request): return 'bar' * request.param @@ -40,8 +51,18 @@ def eat_cucumbers(start_cucumbers, start, eat): start_cucumbers['eat'] = eat +@when('I do not eat any cucumber') +def do_not_eat_any_cucumber(): + pass + + @then('I should have cucumbers') def should_have_left_cucumbers(start_cucumbers, start, eat, left): assert start - eat == left assert start_cucumbers['start'] == start assert start_cucumbers['eat'] == eat + + +@then('I still should have cucumbers') +def still_should_have_start_cucumbers(start_cucumbers, start): + assert start_cucumbers['start'] == start diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 1364d667..00000000 --- a/tests/utils.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - - -def get_test_filepath(filepath): - curr_file_dirpath = os.path.dirname(os.path.realpath(__file__)) - return os.path.join(curr_file_dirpath, filepath) - - -def prepare_feature_and_py_files(testdir, feature_file, py_file): - feature_filepath = get_test_filepath(feature_file) - with open(feature_filepath) as feature_file: - feature_content = feature_file.read() - testdir.makefile('.feature', unicode=feature_content) - - py_filepath = get_test_filepath(py_file) - with open(py_filepath) as py_file: - py_content = py_file.read() - testdir.makepyfile(test_gherkin=py_content)