Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PostStepHook is what I think AfterStepHook ought to have been - https://github.com/cucumber/godog/issues/633 #634

Closed
wants to merge 11 commits into from
Closed
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt

## Unreleased

- Provide support for a hooks to inject, replace or remove an error state from a step - ([TBD](https://github.com/cucumber/godog/pull/TBD) - [johnlon](https://github.com/johnlon))
- Provide support for attachments / embeddings including a new example in the examples dir - ([623](https://github.com/cucumber/godog/pull/623) - [johnlon](https://github.com/johnlon))

## [v0.14.1]
Expand Down
2 changes: 1 addition & 1 deletion _examples/attachments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ The example in this directory shows how the godog API is used to add attachments

You must use the '-v' flag or you will not see the cucumber JSON output.

go test -v atttachment_test.go
go test -v attachments_test.go


13 changes: 3 additions & 10 deletions _examples/attachments/attachments_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
package attachments_test

// This example shows how to set up test suite runner with Go subtests and godog command line parameters.
// Sample commands:
// * run all scenarios from default directory (features): go test -test.run "^TestFeatures/"
// * run all scenarios and list subtest names: go test -test.v -test.run "^TestFeatures/"
// * run all scenarios from one feature file: go test -test.v -godog.paths features/nodogs.feature -test.run "^TestFeatures/"
// * run all scenarios from multiple feature files: go test -test.v -godog.paths features/nodogs.feature,features/godogs.feature -test.run "^TestFeatures/"
// * run single scenario as a subtest: go test -test.v -test.run "^TestFeatures/Eat_5_out_of_12$"
// * show usage help: go test -godog.help
// * show usage help if there were other test files in directory: go test -godog.help godogs_test.go
// * run scenarios with multiple formatters: go test -test.v -godog.format cucumber:cuc.json,pretty -test.run "^TestFeatures/"
// This example shows how to attach data to the cucumber reports
// Run the sample with : go test -v attachments_test.go
// Then review the "embeddings" within the JSON emitted on the console.

import (
"context"
Expand Down
13 changes: 13 additions & 0 deletions _examples/intercept/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# An example of intercepting the step result for post-processing

There are situations where it is useful to be able to post-process the outcome of all the steps in a suite in a generic manner in order to manipulate the status result, any errors returned or the context.Context.

In order to facilitate this use case godog provides a seam where is is possible to inject a handler function to manipulate these values.

## Run the example

You must use the '-v' flag or you will not see the cucumber JSON output.

go test -v interceptor_test.go


16 changes: 16 additions & 0 deletions _examples/intercept/features/intercept.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Feature: Intercepting steps in order to manipulate the response
This test suite installs an interceptor that perfoms some manipulation on the test step result.
The manipulation in this test is arbitrary and for illustration purposes.
The interceptor inverts passing/failing results of any steps containing the text FLIP_ME.

Scenario: The trigger word is not present so a pass status should be untouched
When passing step should be passed

Scenario: The trigger word is not present so a fail status should be untouched
When failing step should be failed

Scenario: Trigger word should should flip a fail to a pass
When failing step with the word FLIP_ME should be passed

Scenario: Trigger word should should flip a pass to a fail
When passing step with the word FLIP_ME should be failed
81 changes: 81 additions & 0 deletions _examples/intercept/interceptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package interceptor_test

// This example shows how to manipulate the results of test steps in a generic manner.
// Run the sample with : go test -v interceptor_test.go
// Then review the json report and confirm that the statuss report make sense given the scenarios.

import (
"context"
"fmt"
"os"
"strings"
"testing"

"github.com/cucumber/godog"
"github.com/cucumber/godog/colors"
)

var opts = godog.Options{
Output: colors.Colored(os.Stdout),
Format: "cucumber", // cucumber json format
}

func TestFeatures(t *testing.T) {
o := opts
o.TestingT = t

status := godog.TestSuite{
Name: "intercept",
Options: &o,
ScenarioInitializer: InitializeScenario,
}.Run()

if status == 2 {
t.SkipNow()
}

if status != 0 {
t.Fatalf("zero status code expected, %d received", status)
}
}

func InitializeScenario(ctx *godog.ScenarioContext) {

ctx.StepContext().Post(func(ctx context.Context, st *godog.Step, status godog.StepResultStatus, err error) (context.Context, godog.StepResultStatus, error) {
if strings.Contains(st.Text, "FLIP_ME") {
if status == godog.StepFailed {
fmt.Printf("FLIP_ME to PASSED\n")
status = godog.StepPassed
err = nil // knock out any error too
} else if status == godog.StepPassed {
fmt.Printf("FLIP_ME to FAILED\n")
status = godog.StepFailed
err = fmt.Errorf("FLIP_ME to FAILED")
} else {
fmt.Printf("FLIP_ME but stays %v\n", status)
}
} else {
fmt.Printf("NOT FLIP_ME so stays %v\n", status)
}
fmt.Printf("FINAL STATUS %v, %v\n", status, err)
return ctx, status, err
})

ctx.Step(`^passing step should be passed$`, func(ctx context.Context) (context.Context, error) {
// this is a pass and we hope it will be reported as a pass
return ctx, nil
})
ctx.Step(`^failing step should be failed$`, func(ctx context.Context) (context.Context, error) {
// this is a fail and we hope it will be reported as a fail
return ctx, fmt.Errorf("intentional failure")
})
ctx.Step(`^passing step with the word FLIP_ME should be failed$`, func(ctx context.Context) (context.Context, error) {
// this is a pass but we hope it will be reported as a fail
return ctx, nil
})
ctx.Step(`^failing step with the word FLIP_ME should be passed$`, func(ctx context.Context) (context.Context, error) {
// this is a fail but we hope it will be reported as a pass
return ctx, fmt.Errorf("this failure should be flipped to passed")
})

}
21 changes: 18 additions & 3 deletions suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type suite struct {
// suite event handlers
beforeScenarioHandlers []BeforeScenarioHook
beforeStepHandlers []BeforeStepHook
postStepHandlers []PostStepHook
afterStepHandlers []AfterStepHook
afterScenarioHandlers []AfterScenarioHook
}
Expand Down Expand Up @@ -123,10 +124,9 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
var match *models.StepDefinition

rctx = ctx
status := StepUndefined

// user multistep definitions may panic
defer func() {

if e := recover(); e != nil {
pe, isErr := e.(error)
switch {
Expand Down Expand Up @@ -154,6 +154,8 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
err = getTestingT(ctx).isFailed()
}

status := StepUndefined

switch {
case errors.Is(err, ErrPending):
status = StepPending
Expand All @@ -170,8 +172,11 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, scena
pickledAttachments := pickleAttachments(ctx)
ctx = clearAttach(ctx)

// Run post step handlers.
rctx, status, err = s.runPostStepHooks(ctx, step, status, err)

// Run after step handlers.
rctx, err = s.runAfterStepHooks(ctx, step, status, err)
rctx, err = s.runAfterStepHooks(rctx, step, status, err)

shouldFail := s.shouldFail(err)

Expand Down Expand Up @@ -296,6 +301,14 @@ func (s *suite) runBeforeStepHooks(ctx context.Context, step *Step, err error) (
return ctx, err
}

func (s *suite) runPostStepHooks(ctx context.Context, step *Step, status StepResultStatus, err error) (context.Context, StepResultStatus, error) {
for _, f := range s.postStepHandlers {
ctx, status, err = f(ctx, step, status, err)
}

return ctx, status, err
}

func (s *suite) runAfterStepHooks(ctx context.Context, step *Step, status StepResultStatus, err error) (context.Context, error) {
for _, f := range s.afterStepHandlers {
hctx, herr := f(ctx, step, status, err)
Expand Down Expand Up @@ -462,6 +475,8 @@ func (s *suite) runSubStep(ctx context.Context, text string, def *models.StepDef
status = StepFailed
}

ctx, status, err = s.runPostStepHooks(ctx, st, status, err)

ctx, err = s.runAfterStepHooks(ctx, st, status, err)
}()

Expand Down
19 changes: 18 additions & 1 deletion test_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,36 @@ func (ctx StepContext) Before(h BeforeStepHook) {
// BeforeStepHook defines a hook before step.
type BeforeStepHook func(ctx context.Context, st *Step) (context.Context, error)

// Post registers a function or method
// to be run directly after step to post process the result.
// This function is a decorator the allows complete manipulation of all features of the step results.
// The Post handler differs from an After handler in two important ways:
// - the After handler cannot modify the step status that is chained through the various hooks
// - the After step cannot replace an existing error with a new error, instead it appends errors together
func (ctx StepContext) Post(h PostStepHook) {
ctx.suite.postStepHandlers = append(ctx.suite.postStepHandlers, h)
}

// After registers a function or method
// to be run after every step.
//
// It may be convenient to return a different kind of error
// in order to print more state details which may help
// in case of step failure
// in case of step failure.
// Any errors returned by an After handler do not replace the errors returned by the step but
// instead are prepended to the step error messages.
// If you want to entirely replace the status, error or context returned by the step then use a
// Post handler.
//
// In some cases, for example when running a headless
// browser, to take a screenshot after failure.
func (ctx StepContext) After(h AfterStepHook) {
ctx.suite.afterStepHandlers = append(ctx.suite.afterStepHandlers, h)
}

// PostStepHook defines a hook as a suffix to the step to allow manipulation of step step status, error or context in a generic manner.
type PostStepHook func(ctx context.Context, st *Step, status StepResultStatus, err error) (context.Context, StepResultStatus, error)

// AfterStepHook defines a hook after step.
type AfterStepHook func(ctx context.Context, st *Step, status StepResultStatus, err error) (context.Context, error)

Expand Down
Loading