Repository: go-bdd/gobdd (go)

Issue:
Add before/after step callbacks
**Is your feature request related to a problem? Please describe.**
I have a repetitive set up and teardown code for many step functions.

**Describe the solution you'd like**
- `gobdd.WithBeforeStep(func(ctx gobdd.Context))`
- `gobdd.WithAfterStep(func(ctx gobdd.Context))`

**Describe alternatives you've considered**
Adding a "decorator" to my step functions.

**Additional context**
It would be nice if the default context contains current feature and scenario references.


Pull request:
Add step hooks
closes #118
partially addresses #121 

Patch:
diff --git a/docs/context.md b/docs/context.md
index 72bc34c..51ec36a 100644
--- a/docs/context.md
+++ b/docs/context.md
@@ -30,6 +30,10 @@ fmt.Printf("Hi %s\n", val) // prints "Hi John"
 
 When the data is not provided, the whole test will fail.
 
+#### Predefined keys
+
+The context holds current test state `testing.T`. It is accessible by calling `Context.Get(TestingTKey{})`. This is useful if you need access to the test state from scenario or step hooks.
+
 ## Good practices
 
 It's a good practice to use custom structs as keys instead of strings or any built-in types to avoid collisions between steps using context.
diff --git a/docs/creating-steps.markdown b/docs/creating-steps.markdown
index 0951dd6..8a7c55d 100644
--- a/docs/creating-steps.markdown
+++ b/docs/creating-steps.markdown
@@ -28,6 +28,26 @@ ctx.GetFloat32(myFloatValue{}, 123)
 
 If the `myFloatValue{}` value doesn't exists the `123` will be returned.
 
+## Hooks
+
+There's a possibility to define hooks which might be helpful building useful reporting, visualization, etc.
+
+* `WithBeforeStep(f func(ctx Context))` configures functions that should be executed before every step
+* `WithAfterStep(f func(ctx Context))` configures functions that should be executed after every step
+
+```go
+suite := NewSuite(
+    t,
+    WithBeforeStep(func(ctx Context) {
+        ctx.Set(time.Time{}, time.Now())
+    }),
+    WithAfterStep(func(ctx Context) {
+        start, _ := ctx.Get(time.Time{})
+        log.Printf("step took %s", time.Since(start.(time.Time)))
+    }),
+)
+```
+
 ## Good practices
 
 Steps should be immutable and only communicate through [the context]({{ site.baseurl }}/context.html).
diff --git a/gobdd.go b/gobdd.go
index 05149ea..c1ba9af 100644
--- a/gobdd.go
+++ b/gobdd.go
@@ -18,7 +18,7 @@ import (
 	msgs "github.com/cucumber/messages-go/v12"
 )
 
-// Holds all the information about the suite (options, steps to execute etc)
+// Suite holds all the information about the suite (options, steps to execute etc)
 type Suite struct {
 	t              TestingT
 	steps          []stepDef
@@ -27,17 +27,19 @@ type Suite struct {
 	parameterTypes map[string][]string
 }
 
-// Holds all the information about how the suite or features/steps should be configured
+// SuiteOptions holds all the information about how the suite or features/steps should be configured
 type SuiteOptions struct {
 	featuresPaths  string
 	ignoreTags     []string
 	tags           []string
 	beforeScenario []func(ctx Context)
 	afterScenario  []func(ctx Context)
+	beforeStep     []func(ctx Context)
+	afterStep      []func(ctx Context)
 	runInParallel  bool
 }
 
-// creates a new suite configuration with default values
+// NewSuiteOptions creates a new suite configuration with default values
 func NewSuiteOptions() SuiteOptions {
 	return SuiteOptions{
 		featuresPaths:  "features/*.feature",
@@ -45,6 +47,8 @@ func NewSuiteOptions() SuiteOptions {
 		tags:           []string{},
 		beforeScenario: []func(ctx Context){},
 		afterScenario:  []func(ctx Context){},
+		beforeStep:     []func(ctx Context){},
+		afterStep:      []func(ctx Context){},
 	}
 }
 
@@ -85,6 +89,20 @@ func WithAfterScenario(f func(ctx Context)) func(*SuiteOptions) {
 	}
 }
 
+// WithBeforeStep configures functions that should be executed before every step
+func WithBeforeStep(f func(ctx Context)) func(*SuiteOptions) {
+	return func(options *SuiteOptions) {
+		options.beforeStep = append(options.beforeStep, f)
+	}
+}
+
+// WithAfterStep configures functions that should be executed after every step
+func WithAfterStep(f func(ctx Context)) func(*SuiteOptions) {
+	return func(options *SuiteOptions) {
+		options.afterStep = append(options.afterStep, f)
+	}
+}
+
 // WithIgnoredTags configures which tags should be skipped while executing a suite
 // Every tag has to start with @ otherwise will be ignored
 func WithIgnoredTags(tags []string) func(*SuiteOptions) {
@@ -116,6 +134,9 @@ type TestingT interface {
 	Run(name string, f func(t *testing.T)) bool
 }
 
+// TestingTKey is used to store reference to current *testing.T instance
+type TestingTKey struct{}
+
 // Creates a new suites with given configuration and empty steps defined
 func NewSuite(t TestingT, optionClosures ...func(*SuiteOptions)) *Suite {
 	options := NewSuiteOptions()
@@ -419,12 +440,28 @@ func (s *Suite) callAfterScenarios(ctx Context) {
 	}
 }
 
+func (s *Suite) callBeforeSteps(ctx Context) {
+	for _, f := range s.options.beforeStep {
+		f(ctx)
+	}
+}
+
+func (s *Suite) callAfterSteps(ctx Context) {
+	for _, f := range s.options.afterStep {
+		f(ctx)
+	}
+}
+
 func (s *Suite) runScenario(ctx Context, scenario *msgs.GherkinDocument_Feature_Scenario,
 	bkg *msgs.GherkinDocument_Feature_Background, t *testing.T) {
-	s.callBeforeScenarios(ctx)
-
-	defer s.callAfterScenarios(ctx)
 	t.Run(fmt.Sprintf("%s %s", strings.TrimSpace(scenario.Keyword), scenario.Name), func(t *testing.T) {
+		// NOTE consider passing t as argument to scenario hooks
+		ctx.Set(TestingTKey{}, t)
+		defer ctx.Set(TestingTKey{}, nil)
+
+		s.callBeforeScenarios(ctx)
+		defer s.callAfterScenarios(ctx)
+
 		if bkg != nil {
 			steps := s.getBackgroundSteps(bkg)
 			s.runSteps(ctx, t, steps)
@@ -463,6 +500,13 @@ func (s *Suite) runStep(ctx Context, t *testing.T, step *msgs.GherkinDocument_Fe
 
 	params := def.expr.FindSubmatch([]byte(step.Text))[1:]
 	t.Run(fmt.Sprintf("%s %s", strings.TrimSpace(step.Keyword), step.Text), func(t *testing.T) {
+		// NOTE consider passing t as argument to step hooks
+		ctx.Set(TestingTKey{}, t)
+		defer ctx.Set(TestingTKey{}, nil)
+
+		s.callBeforeSteps(ctx)
+		defer s.callAfterSteps(ctx)
+
 		def.run(ctx, t, params)
 	})
 }


Test patch:
diff --git a/gobdd_test.go b/gobdd_test.go
index b3672fc..598356b 100644
--- a/gobdd_test.go
+++ b/gobdd_test.go
@@ -141,6 +141,36 @@ func TestWithBeforeScenario(t *testing.T) {
 	}
 }
 
+func TestWithAfterStep(t *testing.T) {
+	c := 0
+	suite := NewSuite(t, WithFeaturesPath("features/background.feature"), WithAfterStep(func(ctx Context) {
+		c++
+	}))
+	suite.AddStep(`I add (\d+) and (\d+)`, add)
+	suite.AddStep(`the result should equal (\d+)`, check)
+
+	suite.Run()
+
+	if err := assert.Equals(2, c); err != nil {
+		t.Error(err)
+	}
+}
+
+func TestWithBeforeStep(t *testing.T) {
+	c := 0
+	suite := NewSuite(t, WithFeaturesPath("features/background.feature"), WithBeforeStep(func(ctx Context) {
+		c++
+	}))
+	suite.AddStep(`I add (\d+) and (\d+)`, add)
+	suite.AddStep(`the result should equal (\d+)`, check)
+
+	suite.Run()
+
+	if err := assert.Equals(2, c); err != nil {
+		t.Error(err)
+	}
+}
+
 func TestIgnoredTags(t *testing.T) {
 	suite := NewSuite(t, WithFeaturesPath("features/ignored_tags.feature"), WithIgnoredTags([]string{"@ignore"}))
 	suite.AddStep(`fail the test`, fail)