diff --git a/CHANGES.rst b/CHANGES.rst index 95b5ccf8..0f69666f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,11 @@ Changelog ========= +2.18.0 +------ + +- Add gherkin terminal reporter (spinus + thedrow) + 2.17.1 ------ diff --git a/README.rst b/README.rst index 2813dc38..9f519843 100644 --- a/README.rst +++ b/README.rst @@ -1126,6 +1126,13 @@ To have an output in json format: py.test --cucumberjson= +To enable gherkin-formatted output on terminal, use + +:: + + py.test --gherkin-terminal-reporter + + Test code generation helpers ---------------------------- diff --git a/pytest_bdd/__init__.py b/pytest_bdd/__init__.py index b00fcebc..a5da058b 100644 --- a/pytest_bdd/__init__.py +++ b/pytest_bdd/__init__.py @@ -3,6 +3,6 @@ from pytest_bdd.steps import given, when, then from pytest_bdd.scenario import scenario, scenarios -__version__ = '2.17.1' +__version__ = '2.18.0' __all__ = [given.__name__, when.__name__, then.__name__, scenario.__name__, scenarios.__name__] diff --git a/pytest_bdd/gherkin_terminal_reporter.py b/pytest_bdd/gherkin_terminal_reporter.py new file mode 100644 index 00000000..2fadb499 --- /dev/null +++ b/pytest_bdd/gherkin_terminal_reporter.py @@ -0,0 +1,98 @@ +# -*- encoding: utf-8 -*- + +from _pytest.terminal import TerminalReporter + + +def add_options(parser): + group = parser.getgroup("terminal reporting", "reporting", after="general") + group._addoption( + '--gherkin-terminal-reporter', + action="store_true", + dest="gherkin_terminal_reporter", + default=False, + help=( + "enable gherkin output" + ) + ) + + +def configure(config): + if config.option.gherkin_terminal_reporter: + # Get the standard terminal reporter plugin and replace it with our + current_reporter = config.pluginmanager.getplugin('terminalreporter') + if current_reporter.__class__ != TerminalReporter: + raise Exception("gherkin-terminal-reporter is not compatibile with any other terminal reporter." + "You can use only one terminal reporter." + "Currently '{0}' is used." + "Please decide to use one by deactivating {0} or gherkin-terminal-reporter." + .format(current_reporter.__class__)) + gherkin_reporter = GherkinTerminalReporter(config) + config.pluginmanager.unregister(current_reporter) + config.pluginmanager.register(gherkin_reporter, 'terminalreporter') + if config.pluginmanager.getplugin("dsession"): + raise Exception("gherkin-terminal-reporter is not compatible with 'xdist' plugin.") + + +class GherkinTerminalReporter(TerminalReporter): + + def __init__(self, config): + TerminalReporter.__init__(self, config) + + def pytest_runtest_logstart(self, nodeid, location): + # Prevent locationline from being printed since we already + # show the module_name & in verbose mode the test name. + pass + + def pytest_runtest_logreport(self, report): + rep = report + res = self.config.hook.pytest_report_teststatus(report=rep) + cat, letter, word = res + + if not letter and not word: + # probably passed setup/teardown + return + + if isinstance(word, tuple): + word, word_markup = word + else: + if rep.passed: + word_markup = {'green': True} + elif rep.failed: + word_markup = {'red': True} + elif rep.skipped: + word_markup = {'yellow': True} + feature_markup = {'blue': True} + scenario_markup = word_markup + + if self.verbosity <= 0: + return TerminalReporter.pytest_runtest_logreport(self, rep) + elif self.verbosity == 1: + if hasattr(report, 'scenario'): + self.ensure_newline() + self._tw.write('Feature: ', **feature_markup) + self._tw.write(report.scenario['feature']['name'], **feature_markup) + self._tw.write('\n') + self._tw.write(' Scenario: ', **scenario_markup) + self._tw.write(report.scenario['name'], **scenario_markup) + self._tw.write(' ') + self._tw.write(word, **word_markup) + self._tw.write('\n') + else: + return TerminalReporter.pytest_runtest_logreport(self, rep) + elif self.verbosity > 1: + if hasattr(report, 'scenario'): + self.ensure_newline() + self._tw.write('Feature: ', **feature_markup) + self._tw.write(report.scenario['feature']['name'], **feature_markup) + self._tw.write('\n') + self._tw.write(' Scenario: ', **scenario_markup) + self._tw.write(report.scenario['name'], **scenario_markup) + self._tw.write('\n') + for step in report.scenario['steps']: + self._tw.write(' {} {}\n'.format(step['keyword'], + step['name']), **scenario_markup) + self._tw.write(' ' + word, **word_markup) + self._tw.write('\n\n') + else: + return TerminalReporter.pytest_runtest_logreport(self, rep) + self.stats.setdefault(cat, []).append(rep) diff --git a/pytest_bdd/plugin.py b/pytest_bdd/plugin.py index 33018a99..1da65498 100644 --- a/pytest_bdd/plugin.py +++ b/pytest_bdd/plugin.py @@ -6,6 +6,7 @@ from . import cucumber_json from . import generation from . import reporting +from . import gherkin_terminal_reporter from .fixtures import * @@ -33,12 +34,14 @@ def pytest_addoption(parser): """Add pytest-bdd options.""" cucumber_json.add_options(parser) generation.add_options(parser) + gherkin_terminal_reporter.add_options(parser) @pytest.mark.trylast def pytest_configure(config): """Configure all subplugins.""" cucumber_json.configure(config) + gherkin_terminal_reporter.configure(config) def pytest_unconfigure(config): diff --git a/tests/feature/gherkin_terminal_reporter.feature b/tests/feature/gherkin_terminal_reporter.feature new file mode 100644 index 00000000..2c1ecdeb --- /dev/null +++ b/tests/feature/gherkin_terminal_reporter.feature @@ -0,0 +1,42 @@ +Feature: Gherkin terminal reporter + + Scenario: Should default output be the same as regular terminal reporter + Given there is gherkin scenario implemented + When tests are run + Then output must be formatted the same way as regular one + + Scenario: Should verbose mode enable displaying feature and scenario names rather than test names in a single line + Given there is gherkin scenario implemented + When tests are run with verbose mode + Then output should contain single line feature description + And output should contain single line scenario description + + Scenario: Should verbose mode preserve displaying of regular tests as usual + Given there is non-gherkin scenario implemented + When tests are run with verbose mode + Then output must be formatted the same way as regular one + + Scenario: Should double verbose mode enable displaying of full gherkin scenario description + Given there is gherkin scenario implemented + When tests are run with very verbose mode + Then output must contain full gherkin scenario description + + Scenario: Should error message be displayed when no scenario is found + Given there is gherkin scenario without implementation + When tests are run with any verbosity mode + Then output contains error about missing scenario implementation + + Scenario: Should error message be displayed when no step is found + Given there is gherkin scenario partially implemented + When tests are run with any verbosity mode + Then output contains error about missing step implementation + + Scenario: Should error message be displayed when error occurs during test execution + Given there is gherkin scenario with broken implementation + When tests are run with any verbosity mode + Then output contains error about missing scenario implementation + + Scenario: Should local variables be displayed when --showlocals option is used + Given there is gherkin scenario with broken implementation + When tests are run with --showlocals + Then error traceback contains local variable descriptions diff --git a/tests/feature/test_gherkin_terminal_reporter.py b/tests/feature/test_gherkin_terminal_reporter.py new file mode 100644 index 00000000..992ba84e --- /dev/null +++ b/tests/feature/test_gherkin_terminal_reporter.py @@ -0,0 +1,278 @@ +import re + + +import pytest + +from pytest_bdd import scenario, given, when, then + + +@scenario('gherkin_terminal_reporter.feature', + 'Should default output be the same as regular terminal reporter') +def test_Should_default_output_be_the_same_as_regular_terminal_reporter(): + pass + + +@scenario('gherkin_terminal_reporter.feature', + 'Should verbose mode enable displaying feature and scenario names rather than test names in a single line') +def test_Should_verbose_mode_enable_displaying_feature_and_scenario_names_rather_than_test_names_in_a_single_line(): + pass + + +@scenario('gherkin_terminal_reporter.feature', + 'Should verbose mode preserve displaying of regular tests as usual') +def test_Should_verbose_mode_preserve_displaying_of_regular_tests_as_usual(): + pass + + +@scenario('gherkin_terminal_reporter.feature', + 'Should double verbose mode enable displaying of full gherkin scenario description') +def test_Should_double_verbose_mode_enable_displaying_of_full_gherkin_scenario_description(): + pass + + +@scenario('gherkin_terminal_reporter.feature', + 'Should error message be displayed when no scenario is found') +def test_Should_error_message_be_displayed_when_no_scenario_is_found(verbosity_mode): + pass + + +@scenario('gherkin_terminal_reporter.feature', + 'Should error message be displayed when no step is found') +def test_Should_error_message_be_displayed_when_no_step_is_found(verbosity_mode): + pass + + +@scenario('gherkin_terminal_reporter.feature', + 'Should error message be displayed when error occurs during test execution') +def test_Should_error_message_be_displayed_when_error_occurs_during_test_execution(verbosity_mode): + pass + + +@scenario('gherkin_terminal_reporter.feature', + 'Should local variables be displayed when --showlocals option is used') +def test_Should_local_variables_be_displayed_when___showlocals_option_is_used(): + pass + + +@pytest.fixture(params=[0, 1, 2], + ids=['compact mode', 'line per test', 'verbose']) +def verbosity_mode(request): + return request.param, '-' + 'v' * request.param if request.param else '' + + +@pytest.fixture +def test_execution(): + return {} + + +@given("there is non-gherkin scenario implemented") +def non_gherkin_test(testdir): + testdir.makepyfile(test_regular=""" + def test_1(): + pass + """) + + +@given("there is gherkin scenario implemented") +def gherkin_scenario(testdir): + testdir.makefile('.feature', test=""" + Feature: Gherkin terminal output feature + Scenario: Scenario example 1 + Given there is a bar + When the bar is accessed + Then world explodes + """) + testdir.makepyfile(test_gherkin=""" + import pytest + from pytest_bdd import given, when, scenario, then + + @given('there is a bar') + def a_bar(): + return 'bar' + + @when('the bar is accessed') + def the_bar_is_accessed(): + pass + + @then('world explodes') + def world_explodes(): + pass + + @scenario('test.feature', 'Scenario example 1') + def test_scenario_1(): + pass + """) + + +@when("tests are run") +def tests_are_run(testdir, test_execution): + test_execution['regular'] = testdir.runpytest() + test_execution['gherkin'] = testdir.runpytest('--gherkin-terminal-reporter') + + +@then("output must be formatted the same way as regular one") +def output_must_be_the_same_as_regular_reporter(test_execution): + reg = test_execution['regular'] + ghe = test_execution['gherkin'] + assert reg.ret == 0 + assert ghe.ret == 0 + # last line can be different because of test execution time is printed + reg_lines = reg.stdout.lines if reg.stdout.lines[-1] else reg.stdout.lines[:-2] + reg_lines[-1] = re.sub(r' \d+\.\d+ ', ' X ', reg_lines[-1]) + ghe_lines = ghe.stdout.lines if ghe.stdout.lines[-1] else ghe.stdout.lines[:-2] + ghe_lines[-1] = re.sub(r' \d+\.\d+ ', ' X ', ghe_lines[-1]) + for l1, l2 in zip(reg_lines, ghe_lines): + assert l1 == l2 + + +@when("tests are run with verbose mode") +def tests_are_run_with_verbose_mode(testdir, test_execution): + test_execution['regular'] = testdir.runpytest('-v') + test_execution['gherkin'] = testdir.runpytest('--gherkin-terminal-reporter', '-v') + + +@when("tests are run with very verbose mode") +def tests_are_run_with_very_verbose_mode(testdir, test_execution): + test_execution['regular'] = testdir.runpytest('-vv') + test_execution['gherkin'] = testdir.runpytest('--gherkin-terminal-reporter', '-vv') + + +@then("output should contain single line feature description") +def output_should_contain_single_line_feature_description(test_execution): + ghe = test_execution['gherkin'] + assert ghe.ret == 0 + ghe.stdout.fnmatch_lines('Feature: Gherkin terminal output feature') + + +@then("output should contain single line scenario description") +def output_should_contain_single_line_scenario_description(test_execution): + ghe = test_execution['gherkin'] + assert ghe.ret == 0 + ghe.stdout.fnmatch_lines('*Scenario: Scenario example 1 PASSED') + + +@then("output must contain full gherkin scenario description") +def output_should_contain_full_gherkin_scenario_description(test_execution): + ghe = test_execution['gherkin'] + assert ghe.ret == 0 + ghe.stdout.fnmatch_lines('*Scenario: Scenario example 1') + ghe.stdout.fnmatch_lines('*Given there is a bar') + ghe.stdout.fnmatch_lines('*When the bar is accessed') + ghe.stdout.fnmatch_lines('*Then world explodes') + ghe.stdout.fnmatch_lines('*PASSED') + + +@given('there is gherkin scenario without implementation') +def gherkin_scenario_without_implementation(testdir): + testdir.makefile('.feature', test=""" + Feature: Gherkin terminal output feature + Scenario: Scenario example 1 + Given there is a bar + When the bar is accessed + Then world explodes + """) + testdir.makepyfile(test_gherkin=""" + import pytest + from pytest_bdd import scenarios + + scenarios('.') + + """) + + +@when('tests are run with any verbosity mode') +def tests_are_run_with_any_verbosity_mode( + test_execution, verbosity_mode, testdir, + gherkin_scenario_without_implementation): + # test_execution['gherkin'] = testdir.runpytest( + # '--gherkin-terminal-reporter', '-vv') + if verbosity_mode[1]: + test_execution['gherkin'] = testdir.runpytest( + '--gherkin-terminal-reporter', verbosity_mode[1]) + else: + test_execution['gherkin'] = testdir.runpytest( + '--gherkin-terminal-reporter') + + +@then('output contains error about missing scenario implementation') +def output_contains_error_about_missing_scenario_implementation(test_execution): + ghe = test_execution['gherkin'] + assert ghe.ret + ghe.stdout.fnmatch_lines('''*StepDefinitionNotFoundError: Step definition is not found: Given "there is a bar". ''' + '''Line 3 in scenario "Scenario example 1"*''') + + +@given('there is gherkin scenario partially implemented') +def partially_implemented_gherkin_scenario(testdir): + testdir.makefile('.feature', test=""" + Feature: Gherkin terminal output feature + Scenario: Scenario example 1 + Given there is a bar + When the bar is accessed + Then world explodes + """) + testdir.makepyfile(test_gherkin=""" + import pytest + from pytest_bdd import given, when, scenario, then + + @given('there is a bar') + def a_bar(): + return 'bar' + + @when('the bar is accessed') + def the_bar_is_accessed(): + pass + + @scenario('test.feature', 'Scenario example 1') + def test_scenario_1(): + pass + """) + + +@then('output contains error about missing step implementation') +def output_contains_error_about_missing_step_implementation(test_execution): + ghe = test_execution['gherkin'] + assert ghe.ret + ghe.stdout.fnmatch_lines('''*StepDefinitionNotFoundError: Step definition is not found: Given "there is a bar". ''' + '''Line 3 in scenario "Scenario example 1"*''') + + +@given('there is gherkin scenario with broken implementation') +def there_is_gherkin_scenario_with_broken_implementation(testdir): + testdir.makefile('.feature', test=""" + Feature: Gherkin terminal output feature + Scenario: Scenario example 1 + Given there is a bar + When the bar is accessed + Then world explodes + """) + testdir.makepyfile(test_gherkin=""" + import pytest + from pytest_bdd import given, when, scenario, then + + @given('there is a bar') + def a_bar(request): + return 'bar' + + @when('the bar is accessed') + def the_bar_is_accessed(request): + local_var = 'value2' + raise Exception("ERROR") + + @scenario('test.feature', 'Scenario example 1') + def test_scenario_1(): + pass + """) + + +@when('tests are run with --showlocals') +def tests_are_run_with___showlocals(test_execution, testdir): + test_execution['gherkin'] = testdir.runpytest('--gherkin-terminal-reporter', '--showlocals') + + +@then('error traceback contains local variable descriptions') +def error_traceback_contains_local_variable_descriptions(test_execution): + ghe = test_execution['gherkin'] + assert ghe.ret + ghe.stdout.fnmatch_lines('''request*=*