Skip to content

Commit

Permalink
Add BeforeAll and AfterAll hooks (#1569)
Browse files Browse the repository at this point in the history
* Add a feature for BeforeAll hook

* Add BeforeAll hook

* Add an AfterAll_hook feature

* Add AfterAll hook

* Move all hook related features into a dedicated sub-folder

* Add a scenario for AfterAll to make sure it is invoked even after failed scenario

* [skip ci] Update CHANGELOG.md

* Draft a documentation for hooks

* Add some sugar to hooks README

* Adlinks to new hooks documentation in CHANGELOG.md

* Do not execute BeforeAll and AfterAll hooks in dry-run mode
  • Loading branch information
aurelien-reeves authored Sep 6, 2021
1 parent ce1ba5f commit 1ecac46
Show file tree
Hide file tree
Showing 14 changed files with 333 additions and 0 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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))

Expand Down
31 changes: 31 additions & 0 deletions features/docs/cli/dry_run.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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)
"""
127 changes: 127 additions & 0 deletions features/docs/writing_support_code/hooks/README.md
Original file line number Diff line number Diff line change
@@ -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
```
80 changes: 80 additions & 0 deletions features/docs/writing_support_code/hooks/after_all_hook.feature
Original file line number Diff line number Diff line change
@@ -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)
"""
49 changes: 49 additions & 0 deletions features/docs/writing_support_code/hooks/before_all_hook.feature
Original file line number Diff line number Diff line change
@@ -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)
"""
12 changes: 12 additions & 0 deletions lib/cucumber/glue/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tt>Given</tt>, <tt>When</tt> and <tt>Then</tt>, and
# also to the i18n translations whenever a feature of a
Expand Down
12 changes: 12 additions & 0 deletions lib/cucumber/glue/registry_and_more.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions lib/cucumber/runtime.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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|
Expand Down

0 comments on commit 1ecac46

Please sign in to comment.