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|