Skip to content

Commit

Permalink
add BeforeStep and AfterStep hooks (#1416)
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam-ARK authored Nov 16, 2020
1 parent b90fc80 commit 4e6a9ee
Show file tree
Hide file tree
Showing 15 changed files with 777 additions and 7 deletions.
30 changes: 28 additions & 2 deletions docs/support_files/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ Defines a hook which is run after each scenario.
* `tags`: String tag expression used to apply this hook to only specific scenarios. See [cucumber-tag-expressions](https://docs.cucumber.io/tag-expressions/) for more information.
* `timeout`: A hook-specific timeout, to override the default timeout.
* `fn`: A function, defined as follows:
* The first argument will be an object of the form `{sourceLocation: {line, uri}, result: {duration, status, exception?}, pickle}`
* The pickle object comes from the [gherkin](https://github.com/cucumber/cucumber/tree/gherkin-v4.1.3/gherkin) library. See `testdata/good/*.pickles.ndjson` for examples of its structure.
* The first argument will be an object of the form `{pickle, gherkinDocument, result, testCaseStartedId}`
* The pickle object comes from the [gherkin](https://github.com/cucumber/cucumber/tree/gherkin/v15.0.2/gherkin) library. See `testdata/good/*.pickles.ndjson` for examples of its structure.
* When using the asynchronous callback interface, have one final argument for the callback function.

`options` can also be a string as a shorthand for specifying `tags`.
Expand All @@ -59,6 +59,24 @@ Multiple `AfterAll` hooks are executed in the **reverse** order that they are de

---

#### `AfterStep([options,] fn)`

Defines a hook which is run after each step.

* `options`: An object with the following keys:
* `tags`: String tag expression used to apply this hook to only specific scenarios. See [cucumber-tag-expressions](https://docs.cucumber.io/tag-expressions/) for more information.
* `timeout`: A hook-specific timeout, to override the default timeout.
* `fn`: A function, defined as follows:
* The first argument will be an object of the form `{pickle, gherkinDocument, result, testCaseStartedId, testStepId}`
* The pickle object comes from the [gherkin](https://github.com/cucumber/cucumber/tree/gherkin/v15.0.2/gherkin) library. See `testdata/good/*.pickles.ndjson` for examples of its structure.
* When using the asynchronous callback interface, have one final argument for the callback function.

`options` can also be a string as a shorthand for specifying `tags`.

Multiple `AfterStep` hooks are executed in the **reverse** order that they are defined.

---

#### `Before([options,] fn)`

Defines a hook which is run before each scenario. Same interface as `After` except the first argument passed to `fn` will not have the `result` property.
Expand All @@ -75,6 +93,14 @@ Multiple `BeforeAll` hooks are executed in the order that they are defined.

---

#### `BeforeStep([options,] fn)`

Defines a hook which is run before each step. Same interface as `AfterStep` except the first argument passed to `fn` will not have the `result` property.

Multiple `BeforeStep` hooks are executed in the order that they are defined.

---

#### `defineStep(pattern[, options], fn)`

Defines a step.
Expand Down
20 changes: 20 additions & 0 deletions docs/support_files/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,23 @@ AfterAll(function () {
return Promise.resolve()
});
```

## 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.

```javascript
var {AfterStep, BeforeStep} = require('cucumber');

BeforeStep({tags: "@foo"}, function () {
// This hook will be executed before all steps in a scenario with tag @foo
});

AfterStep( function ({result}) {
// This hook will be executed after all steps, and take a screenshot on step failure
if (result.status === Status.FAILED) {
this.driver.takeScreenshot();
}
});
```

95 changes: 95 additions & 0 deletions features/before_after_step_hooks.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
Feature: Before and After Step Hooks

Background:
Given a file named "features/a.feature" with:
"""
Feature: some feature
@this-tag
Scenario: some scenario
Given a step
"""
And a file named "features/step_definitions/cucumber_steps.js" with:
"""
const {Given} = require('@cucumber/cucumber')
Given(/^a step$/, function() {})
"""

Scenario: Before and After Hooks work correctly
Given a file named "features/support/hooks.js" with:
"""
const {BeforeStep, AfterStep, BeforeAll, AfterAll} = require('@cucumber/cucumber')
const {expect} = require('chai')
let counter = 1
BeforeStep(function() {
counter = counter + 1
})
AfterStep(function() {
expect(counter).to.eql(2)
counter = counter + 1
})
AfterAll(function() {
expect(counter).to.eql(3)
})
"""
When I run cucumber-js
Then it passes

Scenario: Failing before step fails the scenario
Given a file named "features/support/hooks.js" with:
"""
const {BeforeStep} = require('@cucumber/cucumber')
BeforeStep(function() { throw 'Fail' })
"""
When I run cucumber-js
Then it fails

Scenario: Failing after step fails the scenario
Given a file named "features/support/hooks.js" with:
"""
const {AfterStep} = require('@cucumber/cucumber')
AfterStep(function() { throw 'Fail' })
"""
When I run cucumber-js
Then it fails

Scenario: Only run BeforeStep hooks with appropriate tags
Given a file named "features/support/hooks.js" with:
"""
const { BeforeStep } = require('@cucumber/cucumber')
BeforeStep({tags: "@any-tag"}, function() {
throw Error("Would fail if ran")
})
"""
When I run cucumber-js
Then it passes

Scenario: Only run BeforeStep hooks with appropriate tags
Given a file named "features/support/hooks.js" with:
"""
const { AfterStep } = require('@cucumber/cucumber')
AfterStep({tags: "@this-tag"}, function() {
throw Error("Would fail if ran")
})
"""
When I run cucumber-js
Then it fails

Scenario: after hook parameter can access result status of step
Given a file named "features/support/hooks.js" with:
"""
const { AfterStep, Status } = require('@cucumber/cucumber')
AfterStep(function({result}) {
if (result.status === Status.PASSED) {
return
} else {
throw Error("Result object did not get passed properly to AfterStep Hook.")
}
})
"""
When I run cucumber-js
Then it passes
68 changes: 68 additions & 0 deletions features/world_in_hooks.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
Feature: World in Hooks

Background:
Given a file named "features/a.feature" with:
"""
Feature: some feature
Scenario: some scenario
Given a step
"""
And a file named "features/step_definitions/cucumber_steps.js" with:
"""
const {Given} = require('@cucumber/cucumber')
Given(/^a step$/, function() {})
"""
And a file named "features/support/world.js" with:
"""
const {setWorldConstructor} = require('@cucumber/cucumber')
function WorldConstructor() {
return {
isWorld: function() { return true }
}
}
setWorldConstructor(WorldConstructor)
"""

Scenario: World is this in hooks
Given a file named "features/support/hooks.js" with:
"""
const {After, Before } = require('@cucumber/cucumber')
Before(function() {
if (!this.isWorld()) {
throw Error("Expected this to be world")
}
})
After(function() {
if (!this.isWorld()) {
throw Error("Expected this to be world")
}
})
"""
When I run cucumber-js
Then it passes

Scenario: World is this in BeforeStep hooks
Given a file named "features/support/hooks.js" with:
"""
const {BeforeStep } = require('@cucumber/cucumber')
BeforeStep(function() {
if (!this.isWorld()) {
throw Error("Expected this to be world")
}
})
"""
When I run cucumber-js
Then it passes

Scenario: World is this in AfterStep hooks
Given a file named "features/support/hooks.js" with:
"""
const {AfterStep } = require('@cucumber/cucumber')
AfterStep(function() {
if (!this.isWorld()) {
throw Error("Expected this to be world")
}
})
"""
When I run cucumber-js
Then it passes
2 changes: 2 additions & 0 deletions src/cli/helpers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ function testEmitSupportCodeMessages(
stepDefinitions: [],
beforeTestRunHookDefinitions: [],
beforeTestCaseHookDefinitions: [],
beforeTestStepHookDefinitions: [],
afterTestRunHookDefinitions: [],
afterTestCaseHookDefinitions: [],
afterTestStepHookDefinitions: [],
defaultTimeout: 0,
parameterTypeRegistry: new ParameterTypeRegistry(),
undefinedParameterTypes: [],
Expand Down
43 changes: 43 additions & 0 deletions src/formatter/helpers/summary_helpers_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,5 +351,48 @@ describe('SummaryHelpers', () => {
)
})
})

describe('with one passing scenario with one step and a beforeStep and afterStep hook', () => {
it('outputs the duration as `0m24.000s (executing steps: 0m24.000s)`', async () => {
// Arrange
const sourceData = [
'Feature: a',
'Scenario: b',
'Given a passing step',
].join('\n')
const supportCodeLibrary = buildSupportCodeLibrary(
({ Given, BeforeStep, AfterStep }) => {
Given('a passing step', () => {
clock.tick(12.3 * 1000)
})
BeforeStep(() => {
clock.tick(5 * 1000)
})
AfterStep(() => {
clock.tick(6.7 * 1000)
})
}
)

// Act
const output = await testFormatSummary({
sourceData,
supportCodeLibrary,
testRunFinished: messages.TestRunFinished.fromObject({
timestamp: {
nanos: 0,
seconds: 24,
},
}),
})

// Assert
expect(output).to.contain(
'1 scenario (1 passed)\n' +
'1 step (1 passed)\n' +
'0m24.000s (executing steps: 0m24.000s)\n'
)
})
})
})
})
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export { default as UsageFormatter } from './formatter/usage_formatter'
export { default as UsageJsonFormatter } from './formatter/usage_json_formatter'
export { formatterHelpers }

// Support Code Fuctions
// Support Code Functions
const { methods } = supportCodeLibraryBuilder
export const After = methods.After
export const AfterAll = methods.AfterAll
export const AfterStep = methods.AfterStep
export const Before = methods.Before
export const BeforeAll = methods.BeforeAll
export const BeforeStep = methods.BeforeStep
export const defineParameterType = methods.defineParameterType
export const defineStep = methods.defineStep
export const Given = methods.Given
Expand Down
37 changes: 37 additions & 0 deletions src/models/test_step_hook_definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { PickleTagFilter } from '../pickle_filter'
import Definition, {
IDefinition,
IGetInvocationDataResponse,
IGetInvocationDataRequest,
IDefinitionParameters,
IHookDefinitionOptions,
} from './definition'
import { messages } from '@cucumber/messages'

export default class TestStepHookDefinition
extends Definition
implements IDefinition {
public readonly tagExpression: string
private readonly pickleTagFilter: PickleTagFilter

constructor(data: IDefinitionParameters<IHookDefinitionOptions>) {
super(data)
this.tagExpression = data.options.tags
this.pickleTagFilter = new PickleTagFilter(data.options.tags)
}

appliesToTestCase(pickle: messages.IPickle): boolean {
return this.pickleTagFilter.matchesAllTagExpressions(pickle)
}

async getInvocationParameters({
hookParameter,
}: IGetInvocationDataRequest): Promise<IGetInvocationDataResponse> {
return await Promise.resolve({
getInvalidCodeLengthMessage: () =>
this.buildInvalidCodeLengthMessage('0 or 1', '2'),
parameters: [hookParameter],
validCodeLengths: [0, 1, 2],
})
}
}
Loading

0 comments on commit 4e6a9ee

Please sign in to comment.