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

Add new contextualized API for hooks and steps #409

Merged
merged 8 commits into from
Aug 3, 2021
Merged

Add new contextualized API for hooks and steps #409

merged 8 commits into from
Aug 3, 2021

Conversation

vearutop
Copy link
Member

@vearutop vearutop commented Jul 23, 2021

Description

This PR introduces new API for hooks and steps that includes context and errors.

Motivation & context

In order to grant more control to test suite user, we can upgrade the API for hooks and steps.
This PR implements new API suggested in #360 (comment).

type ScenarioContext struct {
	suite *suite
}

// StepContext allows registering step hooks.
type StepContext struct {
	suite *suite
}

// Before registers a hook to invoke before scenario.
func (ctx ScenarioContext) Before(h BeforeScenarioHook) {
	ctx.suite.beforeScenarioHandlers = append(ctx.suite.beforeScenarioHandlers, h)
}

// BeforeScenarioHook defines a hook before scenario.
type BeforeScenarioHook func(ctx context.Context, sc *Scenario) (context.Context, error)

// After registers a hook to invoke after scenario.
func (ctx ScenarioContext) After(h AfterScenarioHook) {
	ctx.suite.afterScenarioHandlers = append(ctx.suite.afterScenarioHandlers, h)
}

// AfterScenarioHook defines a hook after scenario.
type AfterScenarioHook func(ctx context.Context, sc *Scenario, err error) (context.Context, error)

// StepContext exposes StepContext of a scenario.
func (ctx *ScenarioContext) StepContext() StepContext {
	return StepContext{suite: ctx.suite}
}

// Before registers a hook to invoke before step.
func (ctx StepContext) Before(h BeforeStepHook) {
	ctx.suite.beforeStepHandlers = append(ctx.suite.beforeStepHandlers, h)
}

// BeforeStepHook defines a hook before step.
type BeforeStepHook func(ctx context.Context, st *Step) (context.Context, error)

// After registers a hook to invoke after step.
func (ctx StepContext) After(h AfterStepHook) {
	ctx.suite.afterStepHandlers = append(ctx.suite.afterStepHandlers, h)
}

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

Original API is left for backwards compatibility with deprecation comments.

Step definitions now optionally support having ctx context.Context as a first argument.

Step definition may have one of the following returns processed with help of reflection:

  • empty (nothing returned)
  • context.Context - newly added
  • error
  • (context.Context, error) - newly added
func thereAreGodogs(context.Context ctx, available int) (context.Context, error) {

The context is chained though all hooks and steps allowing pass state of earlier actions to later ones.

Resolves #360.
Resolves #88.
Resolves #175 with ability to inject Scenario specific data to context and read it back from Step hook.
Resolves #397.
Resolves #370.
Resolves #378.

Type of change

  • New feature (non-breaking change which adds new behaviour)

Note to other contributors

No note.

Update required of cucumber.io/docs

Not sure.

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@codecov
Copy link

codecov bot commented Jul 23, 2021

Codecov Report

Merging #409 (4c4b48e) into main (7d343d4) will decrease coverage by 0.83%.
The diff coverage is 78.08%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #409      +/-   ##
==========================================
- Coverage   83.72%   82.88%   -0.84%     
==========================================
  Files          26       26              
  Lines        2390     2454      +64     
==========================================
+ Hits         2001     2034      +33     
- Misses        296      323      +27     
- Partials       93       97       +4     
Impacted Files Coverage Δ
test_context.go 69.44% <46.66%> (-26.56%) ⬇️
suite.go 86.03% <76.81%> (-4.70%) ⬇️
internal/models/stepdef.go 88.80% <100.00%> (+1.10%) ⬆️
run.go 74.69% <100.00%> (+0.31%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7d343d4...4c4b48e. Read the comment docs.

@vearutop vearutop mentioned this pull request Jul 23, 2021
@vearutop vearutop marked this pull request as ready for review July 29, 2021 10:09
@vearutop vearutop requested review from inluxc and lonnblad July 29, 2021 12:38
@priyankshah217
Copy link

@vearutop can you include an example of sharing data across step definitions?

@vearutop
Copy link
Member Author

vearutop commented Aug 2, 2021

@priyankshah217 please check this example

https://play.golang.org/p/lSnIIlf4Puw

Feature: Passing state
  Scenario: Passing state between steps
    # Saving state of total number of godogs
    Given I have a random number of godogs

    # Reading state to eat total number of godogs
    When I eat all available godogs

    Then there are no godogs left
package main

import (
	"context"
	"errors"
	"io/ioutil"
	"log"
	"math/rand"
	"os"

	"github.com/cucumber/godog"
)

type godogEater struct{ cnt uint32 }

func (ge *godogEater) Add(cnt uint32)    { ge.cnt += cnt }
func (ge *godogEater) Available() uint32 { return ge.cnt }
func (ge *godogEater) Eat(cnt uint32) error {
	if cnt > ge.cnt {
		return errors.New("can't eat more than I have")
	}
	ge.cnt -= cnt
	return nil
}

type cntCtxKey struct{} // Key for a particular context value.

func main() {
	if err := ioutil.WriteFile("/tmp/state.feature", []byte(`
Feature: Passing state
  Scenario: Passing state between steps
    Given I have a random number of godogs

    When I eat all available godogs

    Then there are no godogs left
`), 0600); err != nil {
		log.Fatal(err)
	}
	defer os.Remove("/tmp/state.feature")

	eater := godogEater{}

	suite := godog.TestSuite{
		ScenarioInitializer: func(s *godog.ScenarioContext) {
			// Creating a random number of godog and storing it in context for future reference.
			s.Step("I have a random number of godogs", func(ctx context.Context) context.Context {
				cnt := rand.Uint32()
				println("adding random godogs", cnt)
				eater.Add(cnt)
				return context.WithValue(ctx, cntCtxKey{}, cnt)
			})

			// Getting previously stored number of godogs from context.
			s.Step("I eat all available godogs", func(ctx context.Context) error {
				cnt := ctx.Value(cntCtxKey{}).(uint32)
				println("eating random godogs", cnt)
				return eater.Eat(cnt)
			})

			s.Step("there are no godogs left", func() error {
				if eater.Available() != 0 {
					return errors.New("there are still a few godogs")
				}
				return nil
			})
		},
		Options: &godog.Options{
			Format:   "pretty",
			Strict:   true,
			NoColors: true,
			Paths:    []string{"/tmp/state.feature"},
		},
	}

	if suite.Run() != 0 {
		log.Fatal("non-zero status returned, failed to run feature tests")
	}
}

@priyankshah217
Copy link

@vearutop thanks for your help.

@vearutop vearutop merged commit b1728ff into main Aug 3, 2021
@vearutop vearutop deleted the ctx-hooks branch August 3, 2021 15:48
@zhammer
Copy link

zhammer commented Aug 3, 2021

hey hey this closed out two issues i was following on this repo. not using godog currently but thanks for the work :)

@priyankshah217
Copy link

@vearutop I need some help, I was trying ur example and it worked fine. But have a question related to what if step def contains some parameters and how can we pass context (refer below example)? would you please give me some examples?

s.Step("I have a (\d+) number of godogs", iHaveANumberOfGoDogs)

@vearutop
Copy link
Member Author

vearutop commented Aug 9, 2021

@priyankshah217, you can add context as a first argument to any step definition, so if you have one numeric argument, you can declare step as func(ctx context.Context, cnt int) context.Context, for example:

			// Creating a number of godog and storing it in context for future reference.
			s.Step("I have a (\d+) number of godogs", func(ctx context.Context, cnt int) context.Context {
				println("adding godogs", cnt)
				eater.Add(cnt)
				return context.WithValue(ctx, cntCtxKey{}, cnt)
			})

Please also check examples in release notes: https://github.com/cucumber/godog/blob/main/release-notes/v0.12.0.md#step-definition-improvements.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants