diff --git a/CHANGELOG.md b/CHANGELOG.md index eb72f550f0..ae2d5150c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo ### Added +- New `BeforeAll` and `AfterAll` hooks + + More information about hooks can be found in + [features/docs/writing_support_code/hooks/README.md](./features/docs/writing_support_code/hooks/README.md). + + ([1569](https://github.com/cucumber/cucumber-ruby/pull/1569) + [aurelien-reeves](https://github.com/aurelien-reeves)) + - New hook: `InstallPlugin` It is intended to be used to install an external plugin, like cucumber-ruby-wire. @@ -26,6 +34,9 @@ Please visit [cucumber/CONTRIBUTING.md](https://github.com/cucumber/cucumber/blo See [cucumber-ruby-wire](https://github.com/cucumber/cucumber-ruby-wire/) for a usage example. + More information about hooks can be found in + [features/docs/writing_support_code/hooks/README.md](./features/docs/writing_support_code/hooks/README.md). + ([1564](https://github.com/cucumber/cucumber-ruby/pull/1564) [aurelien-reeves](https://github.com/aurelien-reeves)) diff --git a/features/docs/cli/dry_run.feature b/features/docs/cli/dry_run.feature index d8708b7c11..8d8f2585e4 100644 --- a/features/docs/cli/dry_run.feature +++ b/features/docs/cli/dry_run.feature @@ -84,3 +84,34 @@ Feature: Dry Run 1 step (1 undefined) """ + + Scenario: With BeforeAll and AfterAll hooks + Given a file named "features/test.feature" with: + """ + Feature: + Scenario: + Given this step passes + """ + And the standard step definitions + And a file named "features/step_definitions/support.rb" with: + """ + BeforeAll do + raise "BeforeAll hook error has been raised" + end + + AfterAll do + raise "AfterAll hook error has been raised" + end + """ + When I run `cucumber features/test.feature --publish-quiet --dry-run` + Then it should pass with exactly: + """ + Feature: + + Scenario: # features/test.feature:2 + Given this step passes # features/step_definitions/steps.rb:1 + + 1 scenario (1 skipped) + 1 step (1 skipped) + + """ diff --git a/features/docs/writing_support_code/hooks/README.md b/features/docs/writing_support_code/hooks/README.md new file mode 100644 index 0000000000..358a45d6d2 --- /dev/null +++ b/features/docs/writing_support_code/hooks/README.md @@ -0,0 +1,127 @@ +# Cucumber Hooks + +Cucumber proposes several hooks to let you specify some code to be executed at +different stages of test execution, like before or after the execution of a +scenario. + +Hooks are part of your support code. + +They are executed in the following order: + +- [AfterConfiguration](#afterconfiguration-and-installplugin) +- [InstallPlugin](#afterconfiguration-and-installplugin) +- [BeforeAll](#beforeall-and-afterall) + - Per scenario: + - [Around](#around) + - [Before](#before-and-after) + - Per step: + - [AfterStep](#afterstep) + - [After](#before-and-after) +- [AfterAll](#beforeall-and-afterall) + +You can define as many hooks as you want. If you have several hooks of the same +types - for example, several `BeforeAll` hooks - they will be all executed once. + +Multiple hooks of the same type are executed in the order that they were defined. +If you wish to control this order, use manual requires in `env.rb` - This file is +loaded first - or migrate them all to one `hooks.rb` file. + +## AfterConfiguration and InstallPlugin + +[`AfterConfiguration`](#afterconfiguration) and [`InstallPlugin`](#installplugin) +hooks are dedicated to plugins and are meant to extend Cucumber. For example, +[`AfterConfiguration`](#afterconfiguration) allows you to dynamically change the +configuration before the execution of the tests, and [`InstallPlugin`](#installplugin) +allows to have some code that would have deeper impact on the execution. + +### AfterConfiguration + +**Note:** this is a legacy hook. You may consider using [`InstallPlugin`](#installplugin) instead. + +```ruby +AfterConfiguration do |configuration| + # configuration is an instance of Cucumber::Configuration defined in + # lib/cucumber/configuration.rb. +end +``` + +### InstallPlugin + +In addition to the configuration, `InstallPlugin` also has access to some of Cucumber +internals through a `RegistryWrapper`, defined in +[lib/cucumber/glue/registry_wrapper.rb](../../../../lib/cucumber/glue/registry_wrapper.rb). + +```ruby +InstallPlugin do |configuration, registry| + # configuration is an instance of Cucumber::Configuration defined in + # lib/cucumber/configuration.rb + # + # registry is an instance of Cucumber::Glue::RegistryWrapper defined in + # lib/cucumber/glue/registry_wrapper.rb +end +``` + +You can see an example in the [Cucumber Wire plugin](https://github.com/cucumber/cucumber-ruby-wire). + +## BeforeAll and AfterAll + +`BeforeAll` is executed once before the execution of the first scenario. `AfterAll` +is executed once after the execution of the last scenario. + +These two types of hooks have no parameters. Their purpose is to set-up and/or clean-up +your environment not related to Cucumber, like a database or a browser. + +```ruby +BeforeAll do + # snip +end + +AfterAll do + # snip +end +``` + +## Around + +**Note:** `Around` is a legacy hook and its usage is discouraged in favor of +[`Before` and `After`](#before-and-after) hooks. + +`Around` is a special hook which allows you to have a block syntax. Its original +purpose was to support some databases with only block syntax for transactions. + +```ruby +Around do |scenario, block| + SomeDatabase::begin_transaction do # this is just for illustration + block.call + end +end +``` + +## Before and After + +`Before` is executed before each test case. `After` is executed after each test case. +They both have the test case being executed as a parameter. Within the `After` hook, +the status of the test case is also available. + +```ruby +Before do |test_case| + log test_case.name +end + +After do |test_case| + log test_case.failed? + log test_case.status +end +``` + +## AfterStep + +`AfterStep` is executed after each step of a test. If steps are not executed due +to a previous failure, `AfterStep` won't be executed either. + +```ruby +AfterStep do |result, test_step| + log test_step.inspect # test_step is a Cucumber::Core::Test::Step + log result.inspect # result is a Cucumber::Core::Test::Result +end +``` diff --git a/features/docs/writing_support_code/hooks/after_all_hook.feature b/features/docs/writing_support_code/hooks/after_all_hook.feature new file mode 100644 index 0000000000..6c050d1c52 --- /dev/null +++ b/features/docs/writing_support_code/hooks/after_all_hook.feature @@ -0,0 +1,80 @@ +Feature: AfterAll Hooks + + AfterAll hooks can be used if you have some clean-up to be done after all + scenarios have been executed. + + Scenario: A single AfterAll hook + + An AfterAll hook will be invoked a single time after all the scenarios have + been executed. + + Given a file named "features/f.feature" with: + """ + Feature: AfterAll hook + Scenario: #1 + Then the AfterAll hook has not been called yet + + Scenario: #2 + Then the AfterAll hook has not been called yet + """ + And a file named "features/step_definitions/steps.rb" with: + """ + hookCalled = 0 + + AfterAll do + hookCalled += 1 + + raise "AfterAll hook error has been raised" + end + + Then /^the AfterAll hook has not been called yet$/ do + expect(hookCalled).to eq 0 + end + """ + When I run `cucumber features/f.feature --publish-quiet` + Then it should fail with: + """ + Feature: AfterAll hook + + Scenario: #1 # features/f.feature:2 + Then the AfterAll hook has not been called yet # features/step_definitions/steps.rb:9 + + Scenario: #2 # features/f.feature:5 + Then the AfterAll hook has not been called yet # features/step_definitions/steps.rb:9 + + 2 scenarios (2 passed) + 2 steps (2 passed) + """ + And the output should contain: + """ + AfterAll hook error has been raised (RuntimeError) + """ + + Scenario: It is invoked also when scenario has failed + + Given a file named "features/f.feature" with: + """ + Feature: AfterAll hook + Scenario: failed + Given a failed step + """ + And a file named "features/step_definitions/steps.rb" with: + """ + AfterAll do + raise "AfterAll hook error has been raised" + end + + Given /^a failed step$/ do + expect(0).to eq 1 + end + """ + When I run `cucumber features/f.feature --publish-quiet` + Then it should fail with: + """ + 1 scenario (1 failed) + 1 step (1 failed) + """ + And the output should contain: + """ + AfterAll hook error has been raised (RuntimeError) + """ diff --git a/features/docs/writing_support_code/after_hooks.feature b/features/docs/writing_support_code/hooks/after_hooks.feature similarity index 100% rename from features/docs/writing_support_code/after_hooks.feature rename to features/docs/writing_support_code/hooks/after_hooks.feature diff --git a/features/docs/writing_support_code/after_step_hooks.feature b/features/docs/writing_support_code/hooks/after_step_hooks.feature similarity index 100% rename from features/docs/writing_support_code/after_step_hooks.feature rename to features/docs/writing_support_code/hooks/after_step_hooks.feature diff --git a/features/docs/writing_support_code/around_hooks.feature b/features/docs/writing_support_code/hooks/around_hooks.feature similarity index 100% rename from features/docs/writing_support_code/around_hooks.feature rename to features/docs/writing_support_code/hooks/around_hooks.feature diff --git a/features/docs/writing_support_code/hooks/before_all_hook.feature b/features/docs/writing_support_code/hooks/before_all_hook.feature new file mode 100644 index 0000000000..4cc7dd443e --- /dev/null +++ b/features/docs/writing_support_code/hooks/before_all_hook.feature @@ -0,0 +1,49 @@ +Feature: BeforeAll Hooks + + BeforeAll hooks can be used if you have some set-up to be done before all + scenarios are executed. + + BeforeAll hooks are not aware of your configuration. Use AfterConfiguration if + you need it. + + Scenario: A single BeforeAll hook + + A BeforeAll hook will be invoked a single time before all the scenarios are + executed. + + Given a file named "features/f.feature" with: + """ + Feature: BeforeAll hook + Scenario: #1 + Then the BeforeAll hook has been called + + Scenario: #2 + Then the BeforeAll hook has been called + """ + And a file named "features/step_definitions/steps.rb" with: + """ + hookCalled = 0 + + BeforeAll do + hookCalled += 1 + end + + Then /^the BeforeAll hook has been called$/ do + expect(hookCalled).to eq 1 + end + """ + When I run `cucumber features/f.feature` + Then it should pass with: + """ + Feature: BeforeAll hook + + Scenario: #1 # features/f.feature:2 + Then the BeforeAll hook has been called # features/step_definitions/steps.rb:7 + + Scenario: #2 # features/f.feature:5 + Then the BeforeAll hook has been called # features/step_definitions/steps.rb:7 + + 2 scenarios (2 passed) + 2 steps (2 passed) + + """ diff --git a/features/docs/writing_support_code/before_hook.feature b/features/docs/writing_support_code/hooks/before_hook.feature similarity index 100% rename from features/docs/writing_support_code/before_hook.feature rename to features/docs/writing_support_code/hooks/before_hook.feature diff --git a/features/docs/writing_support_code/hook_order.feature b/features/docs/writing_support_code/hooks/hook_order.feature similarity index 100% rename from features/docs/writing_support_code/hook_order.feature rename to features/docs/writing_support_code/hooks/hook_order.feature diff --git a/features/docs/post_configuration_hook.feature b/features/docs/writing_support_code/hooks/post_configuration_hook.feature similarity index 100% rename from features/docs/post_configuration_hook.feature rename to features/docs/writing_support_code/hooks/post_configuration_hook.feature diff --git a/lib/cucumber/glue/dsl.rb b/lib/cucumber/glue/dsl.rb index ce5be37b5b..c32085edd3 100644 --- a/lib/cucumber/glue/dsl.rb +++ b/lib/cucumber/glue/dsl.rb @@ -118,6 +118,18 @@ def InstallPlugin(&proc) Dsl.register_rb_hook('install_plugin', [], proc) end + # Registers a proc that will run before the execution of the scenarios. + # Use it for your final set-ups + def BeforeAll(&proc) + Dsl.register_rb_hook('before_all', [], proc) + end + + # Registers a proc that will run after the execution of the scenarios. + # Use it for your final clean-ups + def AfterAll(&proc) + Dsl.register_rb_hook('after_all', [], proc) + end + # Registers a new Ruby StepDefinition. This method is aliased # to Given, When and Then, and # also to the i18n translations whenever a feature of a diff --git a/lib/cucumber/glue/registry_and_more.rb b/lib/cucumber/glue/registry_and_more.rb index a9a5df4f15..6d7dd681ba 100644 --- a/lib/cucumber/glue/registry_and_more.rb +++ b/lib/cucumber/glue/registry_and_more.rb @@ -146,6 +146,18 @@ def install_plugin(configuration, registry) end end + def before_all + hooks[:before_all].each do |hook| + hook.invoke('BeforeAll', []) + end + end + + def after_all + hooks[:after_all].each do |hook| + hook.invoke('AfterAll', []) + end + end + def add_hook(phase, hook) hooks[phase.to_sym] << hook hook diff --git a/lib/cucumber/runtime.rb b/lib/cucumber/runtime.rb index 440994fa31..1d1088fda9 100644 --- a/lib/cucumber/runtime.rb +++ b/lib/cucumber/runtime.rb @@ -75,12 +75,15 @@ def run! fire_after_configuration_hook fire_install_plugin_hook install_wire_plugin + fire_before_all_hook unless dry_run? # TODO: can we remove this state? self.visitor = report receiver = Test::Runner.new(@configuration.event_bus) compile features, receiver, filters, @configuration.event_bus @configuration.notify :test_run_finished + + fire_after_all_hook unless dry_run? end def features_paths @@ -119,6 +122,14 @@ def fire_install_plugin_hook #:nodoc: @support_code.fire_hook(:install_plugin, @configuration, registry_wrapper) end + def fire_before_all_hook #:nodoc: + @support_code.fire_hook(:before_all) + end + + def fire_after_all_hook #:nodoc: + @support_code.fire_hook(:after_all) + end + require 'cucumber/core/gherkin/document' def features @features ||= feature_files.map do |path|