Skip to content

Commit

Permalink
New Feature: shared context in beforeAll and afterAll hooks (#1770)
Browse files Browse the repository at this point in the history
Co-authored-by: Mona Ghassemi <[email protected]>
Co-authored-by: Matt Wynne <[email protected]>
Co-authored-by: Matt Wynne <[email protected]>
Co-authored-by: Mona Ghassemi <[email protected]>
Co-authored-by: David Goss <[email protected]>
  • Loading branch information
6 people authored Dec 21, 2023
1 parent 96a65ca commit 739fca6
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 7 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
Please see [CONTRIBUTING.md](./CONTRIBUTING.md) on how to contribute to Cucumber.

## [Unreleased]
### Added
- Ability to access World parameters from `BeforeAll`/`AfterAll` hooks (see [documentation](./docs/support_files/hooks.md#world-parameters-in-beforeallafterall)) ([#1770](https://github.com/cucumber/cucumber-js/pull/1770))

### Fixed
- Prevent mutations on world parameters leaking between test cases ([#2362](https://github.com/cucumber/cucumber-js/pull/2362))

Expand Down
21 changes: 19 additions & 2 deletions docs/support_files/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ Before(function() {

If you have some setup / teardown that needs to be done before or after all scenarios, use `BeforeAll` / `AfterAll`. Like hooks and steps, these can be synchronous, accept a callback, or return a promise.

Unlike `Before` / `After` these methods will not have a world instance as `this`. This is because each scenario gets its own world instance and these hooks run before / after **all** scenarios.

```javascript
const {AfterAll, BeforeAll} = require('@cucumber/cucumber');

Expand All @@ -122,6 +120,25 @@ AfterAll(function () {
});
```

### World parameters in BeforeAll/AfterAll

ℹ️ Added in v10.1.0

`BeforeAll`/`AfterAll` hooks aren't given a World instance bound to `this` like other hooks and steps. But they can access [World parameters](./world.md#world-parameters) via `this.parameters` in order to:

- Use the parameters as configuration to drive automation
- Update the parameters with extra context which will then be available to other hooks and steps

Here's a fictional example of obtaining an auth token that can then be used by all tests:

```javascript
const {AfterAll, BeforeAll} = require('@cucumber/cucumber');

BeforeAll(async function () {
this.parameters.accessToken = await getAccessToken(this.parameters.oauth)
});
```

## BeforeStep / AfterStep

If you have some code execution that needs to be done before or after all steps, use `BeforeStep` / `AfterStep`. Like the `Before` / `After` hooks, these also have a world instance as 'this', and can be conditionally selected for execution based on the tags of the scenario.
Expand Down
2 changes: 2 additions & 0 deletions docs/support_files/world.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ The `worldParameters` configuration option allows you to provide this informatio

This option is repeatable, so you can use it multiple times and the objects will be merged with the later ones taking precedence.

Parameters can be modified in `BeforeAll` [hooks](./hooks.md) if required.

## Custom worlds

You might also want to have methods on your world that hooks and steps can access to keep their own code simple. To do this, you can write your own world implementation with its own properties and methods that help with your instrumentation, and then call `setWorldConstructor` to tell Cucumber about it:
Expand Down
112 changes: 112 additions & 0 deletions features/before_after_all_hooks_context.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
Feature: Before/After All Hooks Context

It should be possible to preserve context from a BeforeAll hook
and have the context be available to the scenarios in the World

Background:
Given a file named "cucumber.json" with:
"""
{
"default": {
"worldParameters": {
"widgets": true
}
}
}
"""
And a file named "features/a.feature" with:
"""
Feature: some feature
Scenario: first scenario
Given first step
Scenario: second scenario
Given second step
"""

Scenario: BeforeAll hooks can update world parameters before tests start
Given a file named "features/support/hooks.js" with:
"""
const {AfterAll, BeforeAll, Given} = require('@cucumber/cucumber')
const {expect} = require('chai')
BeforeAll(function() {
expect(this.parameters).to.deep.eq({
widgets: true
})
this.parameters.foo = 1
})
Given('first step', function() {
expect(this.parameters).to.deep.eq({
widgets: true,
foo: 1
})
})
Given('second step', function() {
expect(this.parameters).to.deep.eq({
widgets: true,
foo: 1
})
})
AfterAll(function() {
expect(this.parameters).to.deep.eq({
widgets: true,
foo: 1
})
})
"""
When I run cucumber-js
Then it passes

Scenario: Many BeforeAll hooks can accumulate updates to the world parameters
Given a file named "features/support/hooks.js" with:
"""
const {AfterAll, BeforeAll, Given} = require('@cucumber/cucumber')
const {expect} = require('chai')
BeforeAll(function() {
this.parameters.foo = 1
})
BeforeAll(function() {
this.parameters.bar = 2
})
Given('first step', function() {
expect(this.parameters).to.deep.eq({
widgets: true,
foo: 1,
bar: 2
})
})
Given('second step', function() {})
"""
When I run cucumber-js
Then it passes

Scenario: Works the same way on the parallel runtime
Given a file named "features/support/hooks.js" with:
"""
const {AfterAll, BeforeAll, Given} = require('@cucumber/cucumber')
const {expect} = require('chai')
BeforeAll(function() {
this.parameters.foo = 1
})
Given('first step', function() {
expect(this.parameters).to.deep.eq({
widgets: true,
foo: 1
})
})
Given('second step', function() {})
"""
When I run cucumber-js with `--parallel 2`
Then it passes

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"Michael Lloyd Morris (https://github.com/michael-lloyd-morris)",
"Michael Zedeler <[email protected]>",
"Miika Hänninen <[email protected]>",
"Mona Ghassemi (https://github.com/BlueMona)",
"nebehr <[email protected]>",
"Nico Jansen <[email protected]>",
"Niklas Närhinen <[email protected]>",
Expand Down
1 change: 1 addition & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export default class Runtime implements IRuntime {
this.runTestRunHooks = makeRunTestRunHooks(
this.options.dryRun,
this.supportCodeLibrary.defaultTimeout,
this.options.worldParameters,
(name, location) => `${name} hook errored, process exiting: ${location}`
)
}
Expand Down
1 change: 1 addition & 0 deletions src/runtime/parallel/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export default class Worker {
this.runTestRunHooks = makeRunTestRunHooks(
options.dryRun,
this.supportCodeLibrary.defaultTimeout,
this.worldParameters,
(name, location) =>
`${name} hook errored on worker ${this.id}, process exiting: ${location}`
)
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/run_test_run_hooks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { JsonObject } from 'type-fest'
import UserCodeRunner from '../user_code_runner'
import { formatLocation } from '../formatter/helpers'
import { doesHaveValue, valueOrDefault } from '../value_checker'
Expand All @@ -11,6 +12,7 @@ export type RunsTestRunHooks = (
export const makeRunTestRunHooks = (
dryRun: boolean,
defaultTimeout: number,
worldParameters: JsonObject,
errorMessage: (name: string, location: string) => string
): RunsTestRunHooks =>
dryRun
Expand All @@ -20,7 +22,7 @@ export const makeRunTestRunHooks = (
const { error } = await UserCodeRunner.run({
argsArray: [],
fn: hookDefinition.code,
thisArg: null,
thisArg: { parameters: worldParameters },
timeoutInMilliseconds: valueOrDefault(
hookDefinition.options.timeout,
defaultTimeout
Expand Down
13 changes: 9 additions & 4 deletions src/support_code_library_builder/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as messages from '@cucumber/messages'
import { JsonObject } from 'type-fest'
import TestCaseHookDefinition from '../models/test_case_hook_definition'
import TestStepHookDefinition from '../models/test_step_hook_definition'
import TestRunHookDefinition from '../models/test_run_hook_definition'
Expand Down Expand Up @@ -28,6 +29,10 @@ export interface ITestStepHookParameter {
testStepId: string
}

export type TestRunHookFunction = (this: {
parameters: JsonObject
}) => any | Promise<any>

export type TestCaseHookFunction<WorldType> = (
this: WorldType,
arg: ITestCaseHookParameter
Expand Down Expand Up @@ -108,8 +113,8 @@ export interface IDefineSupportCodeMethods {
options: IDefineTestStepHookOptions,
code: TestStepHookFunction<WorldType>
) => void)
AfterAll: ((code: Function) => void) &
((options: IDefineTestRunHookOptions, code: Function) => void)
AfterAll: ((code: TestRunHookFunction) => void) &
((options: IDefineTestRunHookOptions, code: TestRunHookFunction) => void)
Before: (<WorldType = IWorld>(
code: TestCaseHookFunction<WorldType>
) => void) &
Expand All @@ -132,8 +137,8 @@ export interface IDefineSupportCodeMethods {
options: IDefineTestStepHookOptions,
code: TestStepHookFunction<WorldType>
) => void)
BeforeAll: ((code: Function) => void) &
((options: IDefineTestRunHookOptions, code: Function) => void)
BeforeAll: ((code: TestRunHookFunction) => void) &
((options: IDefineTestRunHookOptions, code: TestRunHookFunction) => void)
Given: IDefineStep
Then: IDefineStep
When: IDefineStep
Expand Down
8 changes: 8 additions & 0 deletions test-d/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ After(async function () {})
BeforeStep(async function () {})
AfterStep(async function () {})

// should allow accessing world parameters in global hooks
BeforeAll(function () {
this.parameters.foo = 1
})
AfterAll(function () {
this.parameters.foo = 1
})

// should allow typed arguments in hooks
Before(function (param: ITestCaseHookParameter) {})
After(function (param: ITestCaseHookParameter) {})
Expand Down

0 comments on commit 739fca6

Please sign in to comment.