Skip to content

Add new contextualized API for hooks and steps#409

Merged
vearutop merged 8 commits intomainfrom
ctx-hooks
Aug 3, 2021
Merged

Add new contextualized API for hooks and steps#409
vearutop merged 8 commits intomainfrom
ctx-hooks

Conversation

@vearutop
Copy link
Copy Markdown
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
Copy Markdown

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
Copy Markdown

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

@vearutop
Copy link
Copy Markdown
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
Copy Markdown

@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
Copy Markdown

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
Copy Markdown

@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
Copy Markdown
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