From 8cbcab640b1652ee5bdbcd16297f762dc0b14efb Mon Sep 17 00:00:00 2001 From: Milosz Sliwinski Date: Sat, 24 Nov 2018 10:43:25 +0100 Subject: [PATCH 1/8] Fixes #271: Use parametrize markers params for getting example_kwargs --- pytest_bdd/reporting.py | 32 ++++++++++++++++++--------- pytest_bdd/utils.py | 30 +++++++++++++++++++++++++ tests/feature/outline_feature.feature | 1 + tests/feature/parametrized.feature | 4 ++++ tests/feature/test_outline.py | 2 +- tests/feature/test_parametrized.py | 11 +++++++++ 6 files changed, 68 insertions(+), 12 deletions(-) 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 7fdfa7f2..c2121cc4 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -110,3 +110,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': _get_param_names(parametrize_args[i]), + 'values': parametrize_args[i+1] + }) + return params + + +def _get_param_names(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 names 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..7febc15b 100644 --- a/tests/feature/parametrized.feature +++ b/tests/feature/parametrized.feature @@ -2,3 +2,7 @@ Scenario: Parametrized given, when, thens Given there are cucumbers When I eat cucumbers Then I should have cucumbers + + +Scenario: Parametrized given - single param + Given there are cucumbers 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..bb8396b4 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] +) +@scenario( + 'parametrized.feature', + 'Parametrized given - single param', +) +def test_parametrized_single_param(request, start): + """Test parametrized scenario.""" + + @pytest.fixture(params=[1, 2]) def foo_bar(request): return 'bar' * request.param From e0167919f00409f3975d9308a66d1b23ccad30bc Mon Sep 17 00:00:00 2001 From: Milosz Sliwinski Date: Sun, 25 Nov 2018 13:18:06 +0100 Subject: [PATCH 2/8] _get_param_names function renamed to _coerce_list --- pytest_bdd/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index c2121cc4..85e8c15e 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -128,13 +128,13 @@ def get_parametrize_params(parametrize_args): params = [] for i in range(0, len(parametrize_args), 2): params.append({ - 'names': _get_param_names(parametrize_args[i]), + 'names': _coerce_list(parametrize_args[i]), 'values': parametrize_args[i+1] }) return params -def _get_param_names(names): +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: From ca0efedaab6887d1864bb649129585d4cfa56060 Mon Sep 17 00:00:00 2001 From: Milosz Sliwinski Date: Sun, 25 Nov 2018 14:22:03 +0100 Subject: [PATCH 3/8] Coerce getting params names as list on get_parametrize_params --- pytest_bdd/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_bdd/utils.py b/pytest_bdd/utils.py index 85e8c15e..1141ef38 100644 --- a/pytest_bdd/utils.py +++ b/pytest_bdd/utils.py @@ -139,4 +139,4 @@ def _coerce_list(names): # As pytest.mark.parametrize has only one param name, # it is not returned as a list. Convert it to list: names = [names] - return names + return list(names) From 646fc1cd23204dc67df57ba2e26d485e8baa4bf4 Mon Sep 17 00:00:00 2001 From: Milosz Sliwinski Date: Wed, 19 Dec 2018 09:52:08 +0100 Subject: [PATCH 4/8] Test parametrized tests in gherkin expanded mode --- .../feature/test_gherkin_terminal_reporter.py | 18 +++++++++--------- tests/utils.py | 11 +++++++++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index fec39189..1f422cee 100644 --- a/tests/feature/test_gherkin_terminal_reporter.py +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -1,9 +1,9 @@ +import os import re - import pytest -from pytest_bdd import scenario, given, when, then +from pytest_bdd import given, scenario, then, when from tests.utils import get_test_filepath, prepare_feature_and_py_files @@ -349,18 +349,18 @@ def output_output_must_contain_parameters_values(test_execution, gherkin_scenari @pytest.mark.parametrize( 'feature_file, py_file, name', [ - ('./steps/unicode.feature', './steps/test_unicode.py', 'test_steps_in_feature_file_have_unicode') + ('./steps/unicode.feature', './steps/test_unicode.py', 'test_steps_in_feature_file_have_unicode'), + ('./feature/parametrized.feature', './feature/test_parametrized.py', 'test_parametrized') ] ) -def test_scenario_in_expanded_mode(testdir, test_execution, feature_file, py_file, name): +def test_scenario_in_expanded_mode(testdir, feature_file, py_file, name): prepare_feature_and_py_files(testdir, feature_file, py_file) - test_execution['gherkin'] = testdir.runpytest( - '-k %s' % name, + py_filename = os.path.basename(py_file) + result = testdir.runpytest( + '%s::%s' % (py_filename, name), '--gherkin-terminal-reporter', '--gherkin-terminal-reporter-expanded', '-vv', ) - - ghe = test_execution['gherkin'] - ghe.assert_outcomes(passed=1) + result.assert_outcomes(passed=1) diff --git a/tests/utils.py b/tests/utils.py index 1364d667..a81188a6 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,14 +5,21 @@ def get_test_filepath(filepath): curr_file_dirpath = os.path.dirname(os.path.realpath(__file__)) return os.path.join(curr_file_dirpath, filepath) +def get_filename_without_ext(path): + filename = os.path.basename(path) + return os.path.splitext(filename)[0] 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) + feature_filename = get_filename_without_ext(feature_file.name) + kwargs = {feature_filename: feature_content} + testdir.makefile('.feature', **kwargs) 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) + py_filename = get_filename_without_ext(py_file.name) + kwargs = {py_filename: py_content} + testdir.makepyfile(**kwargs) From 432e0e05c7af6b925f6450d2800a1d45506d9391 Mon Sep 17 00:00:00 2001 From: Milosz Sliwinski Date: Wed, 19 Dec 2018 11:02:01 +0100 Subject: [PATCH 5/8] Linter fixes --- tests/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/utils.py b/tests/utils.py index a81188a6..cd1eb702 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -5,10 +5,12 @@ def get_test_filepath(filepath): curr_file_dirpath = os.path.dirname(os.path.realpath(__file__)) return os.path.join(curr_file_dirpath, filepath) + def get_filename_without_ext(path): filename = os.path.basename(path) return os.path.splitext(filename)[0] + 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: From 8fef144ddb1c7e40c1fe0276a70cf52a8bebd6d6 Mon Sep 17 00:00:00 2001 From: Milosz Sliwinski Date: Sun, 17 Feb 2019 14:09:45 +0100 Subject: [PATCH 6/8] When and Then steps added to test_parametrized_single_param test --- tests/feature/parametrized.feature | 2 ++ tests/feature/test_parametrized.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/tests/feature/parametrized.feature b/tests/feature/parametrized.feature index 7febc15b..72d2d218 100644 --- a/tests/feature/parametrized.feature +++ b/tests/feature/parametrized.feature @@ -6,3 +6,5 @@ Scenario: Parametrized given, when, thens Scenario: Parametrized given - single param Given there are cucumbers + When I do not eat any cucumber + Then I still should have cucumbers diff --git a/tests/feature/test_parametrized.py b/tests/feature/test_parametrized.py index bb8396b4..9f1c49a1 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -51,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 From cd4d74ae9201bbfe6650c0f1fc878d84b9f4cf05 Mon Sep 17 00:00:00 2001 From: Milosz Sliwinski Date: Sun, 17 Feb 2019 14:21:37 +0100 Subject: [PATCH 7/8] test_parametrized_single_param test renamed to test_parametrized_single_parameter_name --- tests/feature/parametrized.feature | 2 +- tests/feature/test_parametrized.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/feature/parametrized.feature b/tests/feature/parametrized.feature index 72d2d218..dc3779b4 100644 --- a/tests/feature/parametrized.feature +++ b/tests/feature/parametrized.feature @@ -4,7 +4,7 @@ Scenario: Parametrized given, when, thens Then I should have cucumbers -Scenario: Parametrized given - single param +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_parametrized.py b/tests/feature/test_parametrized.py index 9f1c49a1..849b0aa7 100644 --- a/tests/feature/test_parametrized.py +++ b/tests/feature/test_parametrized.py @@ -15,13 +15,13 @@ def test_parametrized(request, start, eat, left): @pytest.mark.parametrize( - 'start', [12] + 'start', [12, 5] ) @scenario( 'parametrized.feature', - 'Parametrized given - single param', + 'Parametrized given, then - single parameter name', ) -def test_parametrized_single_param(request, start): +def test_parametrized_single_parameter_name(request, start): """Test parametrized scenario.""" From 256f6c66593643ee03f566b82ec4bc6369e52b9d Mon Sep 17 00:00:00 2001 From: Milosz Sliwinski Date: Tue, 12 Mar 2019 16:57:46 +0100 Subject: [PATCH 8/8] Rewrite tests of gherkin terminal reporter expanded mode to pytest-bdd style --- .../feature/gherkin_terminal_reporter.feature | 12 ++ .../feature/test_gherkin_terminal_reporter.py | 122 +++++++++++++++--- tests/utils.py | 27 ---- 3 files changed, 114 insertions(+), 47 deletions(-) delete mode 100644 tests/utils.py diff --git a/tests/feature/gherkin_terminal_reporter.feature b/tests/feature/gherkin_terminal_reporter.feature index 4f8d0939..50706eed 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 tests are run 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 tests are run 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 tests are run 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/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py index 1f422cee..d3fd7356 100644 --- a/tests/feature/test_gherkin_terminal_reporter.py +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -1,10 +1,8 @@ -import os +# coding: utf-8 import re import pytest - -from pytest_bdd import given, scenario, then, when -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("tests are run") def tests_are_run(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'), - ('./feature/parametrized.feature', './feature/test_parametrized.py', 'test_parametrized') - ] -) -def test_scenario_in_expanded_mode(testdir, 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() - py_filename = os.path.basename(py_file) - result = testdir.runpytest( - '%s::%s' % (py_filename, name), - '--gherkin-terminal-reporter', - '--gherkin-terminal-reporter-expanded', - '-vv', - ) - result.assert_outcomes(passed=1) + +@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/utils.py b/tests/utils.py deleted file mode 100644 index cd1eb702..00000000 --- a/tests/utils.py +++ /dev/null @@ -1,27 +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 get_filename_without_ext(path): - filename = os.path.basename(path) - return os.path.splitext(filename)[0] - - -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() - feature_filename = get_filename_without_ext(feature_file.name) - kwargs = {feature_filename: feature_content} - testdir.makefile('.feature', **kwargs) - - py_filepath = get_test_filepath(py_file) - with open(py_filepath) as py_file: - py_content = py_file.read() - py_filename = get_filename_without_ext(py_file.name) - kwargs = {py_filename: py_content} - testdir.makepyfile(**kwargs)