From 62d84eedbc1eee1efc169e2b0bedec355517c991 Mon Sep 17 00:00:00 2001 From: Alexandru Chitu Date: Mon, 23 Jan 2023 14:22:19 +0000 Subject: [PATCH] Implement unambiguous keywords (#509) feat(*): create keyword functions * chore(*): update messages and gherkin, relocate Keyword * chore(*): update messages and gherkin, relocate Keyword * chore(*): update messages and gherkin, relocate Keyword * feat(*): mandate keyword type when unambiguous keyword function is used * test(*): keyword type in feature files * docs(*): update step-by-step walkthrough to mention the option of using keyword functions * docs(*): update CHANGELOG.md * test(*): keyword substeps * chore(*): go mod --- CHANGELOG.md | 2 + README.md | 9 ++ features/events.feature | 6 +- features/formatter/pretty.feature | 144 +++++++++++++++++++++++++++++- features/multistep.feature | 37 ++++++++ formatters/fmt.go | 10 +++ run_test.go | 14 +-- suite.go | 35 ++++++-- suite_context_test.go | 17 ++++ test_context.go | 41 +++++++-- 10 files changed, 289 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae66959..c41f47a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ This project adheres to [Semantic Versioning](http://semver.org). This document is formatted according to the principles of [Keep A CHANGELOG](http://keepachangelog.com). ## Unreleased +### Added +- Added keyword functions. ([509](https://github.com/cucumber/godog/pull/509) - [otrava7](https://github.com/otrava7)) ## [v0.12.6] ### Changed diff --git a/README.md b/README.md index 1679f49..b3e1caf 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,15 @@ func InitializeScenario(ctx *godog.ScenarioContext) { } ``` +Alternatively, you can also specify the keyword (Given, When, Then...) when creating the step definitions: +``` go +func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.Given(`^I eat (\d+)$`, iEat) + ctx.When(`^there are (\d+) godogs$`, thereAreGodogs) + ctx.Then(`^there should be (\d+) remaining$`, thereShouldBeRemaining) +} +``` + Our module should now look like this: ``` godogs diff --git a/features/events.feature b/features/events.feature index 8985914..caaca30 100644 --- a/features/events.feature +++ b/features/events.feature @@ -121,17 +121,17 @@ Feature: suite events Feature: scenario hook errors Scenario: failing before and after scenario # normal.feature:3 - Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12 + Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func17 after scenario hook failed: failed in after scenario hook, step error: before scenario hook failed: failed in before scenario hook And passing step # suite_context_test.go:0 -> InitializeScenario.func2 Scenario: failing before scenario # normal.feature:7 - Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12 + Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func17 before scenario hook failed: failed in before scenario hook And passing step # suite_context_test.go:0 -> InitializeScenario.func2 Scenario: failing after scenario # normal.feature:11 - Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func12 + Then adding step state to context # suite_context_test.go:0 -> InitializeScenario.func17 And passing step # suite_context_test.go:0 -> InitializeScenario.func2 after scenario hook failed: failed in after scenario hook diff --git a/features/formatter/pretty.feature b/features/formatter/pretty.feature index 86d10a8..197c1f5 100644 --- a/features/formatter/pretty.feature +++ b/features/formatter/pretty.feature @@ -332,11 +332,11 @@ Feature: pretty formatter Feature: inject long value Scenario: test scenario # features/inject.feature:3 - Given Ignore I save some value X under key Y # suite_context.go:0 -> SuiteContext.func7 + Given Ignore I save some value X under key Y # suite_context.go:0 -> SuiteContext.func12 And I allow variable injection # suite_context.go:0 -> *suiteContext - When Ignore I use value someverylonginjectionsoweacanbesureitsurpasstheinitiallongeststeplenghtanditwillhelptestsmethodsafety # suite_context.go:0 -> SuiteContext.func7 - Then Ignore Godog rendering should not break # suite_context.go:0 -> SuiteContext.func7 - And Ignore test # suite_context.go:0 -> SuiteContext.func7 + When Ignore I use value someverylonginjectionsoweacanbesureitsurpasstheinitiallongeststeplenghtanditwillhelptestsmethodsafety # suite_context.go:0 -> SuiteContext.func12 + Then Ignore Godog rendering should not break # suite_context.go:0 -> SuiteContext.func12 + And Ignore test # suite_context.go:0 -> SuiteContext.func12 | key | val | | 1 | 2 | | 3 | 4 | @@ -548,3 +548,139 @@ Feature: pretty formatter 2 steps (1 passed, 1 failed) 0s """ + + Scenario: Use 'given' keyword on a declared 'when' step + Given a feature "features/simple.feature" file: + """ + Feature: simple feature with a rule + simple feature description + Rule: simple rule + simple rule description + Example: simple scenario + simple scenario description + Given a when step + """ + When I run feature suite with formatter "pretty" + Then the rendered output will be as follows: + """ + Feature: simple feature with a rule + simple feature description + + Example: simple scenario # features/simple.feature:5 + Given a when step + + 1 scenarios (1 undefined) + 1 steps (1 undefined) + 0s + + You can implement step definitions for undefined steps with these snippets: + + func aWhenStep() error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.Step(`^a when step$`, aWhenStep) + } + """ + + Scenario: Use 'when' keyword on a declared 'then' step + Given a feature "features/simple.feature" file: + """ + Feature: simple feature with a rule + simple feature description + Rule: simple rule + simple rule description + Example: simple scenario + simple scenario description + When a then step + """ + When I run feature suite with formatter "pretty" + Then the rendered output will be as follows: + """ + Feature: simple feature with a rule + simple feature description + + Example: simple scenario # features/simple.feature:5 + When a then step + + 1 scenarios (1 undefined) + 1 steps (1 undefined) + 0s + + You can implement step definitions for undefined steps with these snippets: + + func aThenStep() error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.Step(`^a then step$`, aThenStep) + } + """ + + Scenario: Use 'then' keyword on a declared 'given' step + Given a feature "features/simple.feature" file: + """ + Feature: simple feature with a rule + simple feature description + Rule: simple rule + simple rule description + Example: simple scenario + simple scenario description + Then a given step + """ + When I run feature suite with formatter "pretty" + Then the rendered output will be as follows: + """ + Feature: simple feature with a rule + simple feature description + + Example: simple scenario # features/simple.feature:5 + Then a given step + + 1 scenarios (1 undefined) + 1 steps (1 undefined) + 0s + + You can implement step definitions for undefined steps with these snippets: + + func aGivenStep() error { + return godog.ErrPending + } + + func InitializeScenario(ctx *godog.ScenarioContext) { + ctx.Step(`^a given step$`, aGivenStep) + } + """ + + Scenario: Match keyword functions correctly + Given a feature "features/simple.feature" file: + """ + Feature: simple feature with a rule + simple feature description + Rule: simple rule + simple rule description + Example: simple scenario + simple scenario description + Given a given step + When a when step + Then a then step + And a then step + """ + When I run feature suite with formatter "pretty" + Then the rendered output will be as follows: + """ + Feature: simple feature with a rule + simple feature description + + Example: simple scenario # features/simple.feature:5 + Given a given step # suite_context_test.go:0 -> InitializeScenario.func3 + When a when step # suite_context_test.go:0 -> InitializeScenario.func4 + Then a then step # suite_context_test.go:0 -> InitializeScenario.func5 + And a then step # suite_context_test.go:0 -> InitializeScenario.func5 + + 1 scenarios (1 passed) + 4 steps (4 passed) + 0s + """ \ No newline at end of file diff --git a/features/multistep.feature b/features/multistep.feature index a972678..0681b15 100644 --- a/features/multistep.feature +++ b/features/multistep.feature @@ -161,3 +161,40 @@ Feature: run features with nested steps """ When I run feature suite Then the suite should have passed + + Scenario: should run passing multistep using keyword function successfully + Given a feature "normal.feature" file: + """ + Feature: normal feature + + Scenario: run passing multistep + Given passing step + Then passing multistep using 'then' function + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + passing multistep using 'then' function + """ + + Scenario: should identify undefined multistep using keyword function + Given a feature "normal.feature" file: + """ + Feature: normal feature + + Scenario: run passing multistep + Given passing step + Then undefined multistep using 'then' function + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + """ + And the following step should be undefined: + """ + undefined multistep using 'then' function + """ diff --git a/formatters/fmt.go b/formatters/fmt.go index 0c0438e..870bf48 100644 --- a/formatters/fmt.go +++ b/formatters/fmt.go @@ -88,4 +88,14 @@ type FormatterFunc func(string, io.Writer) Formatter type StepDefinition struct { Expr *regexp.Regexp Handler interface{} + Keyword Keyword } + +type Keyword int64 + +const ( + Given Keyword = iota + When + Then + None +) diff --git a/run_test.go b/run_test.go index 9d5cfca..792da0b 100644 --- a/run_test.go +++ b/run_test.go @@ -518,11 +518,12 @@ func Test_AllFeaturesRun(t *testing.T) { ...................................................................... 140 ...................................................................... 210 ...................................................................... 280 -....................................................... 335 +...................................................................... 350 +...... 356 -88 scenarios (88 passed) -335 steps (335 passed) +94 scenarios (94 passed) +356 steps (356 passed) 0s ` @@ -545,11 +546,12 @@ func Test_AllFeaturesRunAsSubtests(t *testing.T) { ...................................................................... 140 ...................................................................... 210 ...................................................................... 280 -....................................................... 335 +...................................................................... 350 +...... 356 -88 scenarios (88 passed) -335 steps (335 passed) +94 scenarios (94 passed) +356 steps (356 passed) 0s ` diff --git a/suite.go b/suite.go index 121b4c4..5321427 100644 --- a/suite.go +++ b/suite.go @@ -65,7 +65,7 @@ type suite struct { } func (s *suite) matchStep(step *messages.PickleStep) *models.StepDefinition { - def := s.matchStepText(step.Text) + def := s.matchStepTextAndType(step.Text, step.Type) if def != nil && step.Argument != nil { def.Args = append(def.Args, step.Argument) } @@ -147,7 +147,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS return ctx, err } - if ctx, undef, err := s.maybeUndefined(ctx, step.Text, step.Argument); err != nil { + if ctx, undef, err := s.maybeUndefined(ctx, step.Text, step.Argument, step.Type); err != nil { return ctx, err } else if len(undef) > 0 { if match != nil { @@ -155,6 +155,7 @@ func (s *suite) runStep(ctx context.Context, pickle *Scenario, step *Step, prevS StepDefinition: formatters.StepDefinition{ Expr: match.Expr, Handler: match.Handler, + Keyword: match.Keyword, }, Args: match.Args, HandlerValue: match.HandlerValue, @@ -297,8 +298,8 @@ func (s *suite) runAfterScenarioHooks(ctx context.Context, pickle *messages.Pick return ctx, err } -func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{}) (context.Context, []string, error) { - step := s.matchStepText(text) +func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{}, stepType messages.PickleStepType) (context.Context, []string, error) { + step := s.matchStepTextAndType(text, stepType) if nil == step { return ctx, []string{text}, nil } @@ -323,7 +324,7 @@ func (s *suite) maybeUndefined(ctx context.Context, text string, arg interface{} if len(lines[0]) > 0 && lines[0][len(lines[0])-1] == ':' { return ctx, undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument") } - ctx, undef, err := s.maybeUndefined(ctx, next, nil) + ctx, undef, err := s.maybeUndefined(ctx, next, nil, messages.PickleStepType_UNKNOWN) if err != nil { return ctx, undefined, err } @@ -349,7 +350,7 @@ func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context. var err error for _, text := range steps { - if def := s.matchStepText(text); def == nil { + if def := s.matchStepTextAndType(text, messages.PickleStepType_UNKNOWN); def == nil { return ctx, ErrUndefined } else if ctx, err = s.maybeSubSteps(def.Run(ctx)); err != nil { return ctx, fmt.Errorf("%s: %+v", text, err) @@ -358,9 +359,12 @@ func (s *suite) maybeSubSteps(ctx context.Context, result interface{}) (context. return ctx, nil } -func (s *suite) matchStepText(text string) *models.StepDefinition { +func (s *suite) matchStepTextAndType(text string, stepType messages.PickleStepType) *models.StepDefinition { for _, h := range s.steps { if m := h.Expr.FindStringSubmatch(text); len(m) > 0 { + if !keywordMatches(h.Keyword, stepType) { + continue + } var args []interface{} for _, m := range m[1:] { args = append(args, m) @@ -372,6 +376,7 @@ func (s *suite) matchStepText(text string) *models.StepDefinition { StepDefinition: formatters.StepDefinition{ Expr: h.Expr, Handler: h.Handler, + Keyword: h.Keyword, }, Args: args, HandlerValue: h.HandlerValue, @@ -382,6 +387,22 @@ func (s *suite) matchStepText(text string) *models.StepDefinition { return nil } +func keywordMatches(k formatters.Keyword, stepType messages.PickleStepType) bool { + if k == formatters.None { + return true + } + switch stepType { + case messages.PickleStepType_CONTEXT: + return k == formatters.Given + case messages.PickleStepType_ACTION: + return k == formatters.When + case messages.PickleStepType_OUTCOME: + return k == formatters.Then + default: + return true + } +} + func (s *suite) runSteps(ctx context.Context, pickle *Scenario, steps []*Step) (context.Context, error) { var ( stepErr, err error diff --git a/suite_context_test.go b/suite_context_test.go index 04f8926..4da8f63 100644 --- a/suite_context_test.go +++ b/suite_context_test.go @@ -73,6 +73,15 @@ func InitializeScenario(ctx *ScenarioContext) { ctx.Step(`^(?:a )?passing step$`, func() error { return nil }) + ctx.Given(`^(?:a )?given step$`, func() error { + return nil + }) + ctx.When(`^(?:a )?when step$`, func() error { + return nil + }) + ctx.Then(`^(?:a )?then step$`, func() error { + return nil + }) // Introduced to test formatter/cucumber.feature ctx.Step(`^the rendered json will be as follows:$`, tc.theRenderJSONWillBe) @@ -91,10 +100,18 @@ func InitializeScenario(ctx *ScenarioContext) { return Steps{"passing step", "undefined step", "passing step"} }) + ctx.Then(`^(?:a |an )?undefined multistep using 'then' function$`, func() Steps { + return Steps{"given step", "undefined step", "then step"} + }) + ctx.Step(`^(?:a )?passing multistep$`, func() Steps { return Steps{"passing step", "passing step", "passing step"} }) + ctx.Then(`^(?:a )?passing multistep using 'then' function$`, func() Steps { + return Steps{"given step", "when step", "then step"} + }) + ctx.Step(`^(?:a )?failing nested multistep$`, func() Steps { return Steps{"passing step", "passing multistep", "failing multistep"} }) diff --git a/test_context.go b/test_context.go index d114c74..f854d20 100644 --- a/test_context.go +++ b/test_context.go @@ -26,12 +26,12 @@ type Step = messages.PickleStep // instead of returning an error in step func // it is possible to return combined steps: // -// func multistep(name string) godog.Steps { -// return godog.Steps{ -// fmt.Sprintf(`an user named "%s"`, name), -// fmt.Sprintf(`user "%s" is authenticated`, name), -// } -// } +// func multistep(name string) godog.Steps { +// return godog.Steps{ +// fmt.Sprintf(`an user named "%s"`, name), +// fmt.Sprintf(`user "%s" is authenticated`, name), +// } +// } // // These steps will be matched and executed in // sequential order. The first one which fails @@ -251,6 +251,34 @@ func (ctx *ScenarioContext) AfterStep(fn func(st *Step, err error)) { // ErrUndefined error will be returned when // running steps. func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { + ctx.stepWithKeyword(expr, stepFunc, formatters.None) +} + +// Given functions identically to Step, but the *StepDefinition +// will only be matched if the step starts with "Given". "And" +// and "But" keywords copy the keyword of the last step for the +// purpose of matching. +func (ctx *ScenarioContext) Given(expr, stepFunc interface{}) { + ctx.stepWithKeyword(expr, stepFunc, formatters.Given) +} + +// When functions identically to Step, but the *StepDefinition +// will only be matched if the step starts with "When". "And" +// and "But" keywords copy the keyword of the last step for the +// purpose of matching. +func (ctx *ScenarioContext) When(expr, stepFunc interface{}) { + ctx.stepWithKeyword(expr, stepFunc, formatters.When) +} + +// Then functions identically to Step, but the *StepDefinition +// will only be matched if the step starts with "Then". "And" +// and "But" keywords copy the keyword of the last step for the +// purpose of matching. +func (ctx *ScenarioContext) Then(expr, stepFunc interface{}) { + ctx.stepWithKeyword(expr, stepFunc, formatters.Then) +} + +func (ctx *ScenarioContext) stepWithKeyword(expr interface{}, stepFunc interface{}, keyword formatters.Keyword) { var regex *regexp.Regexp switch t := expr.(type) { @@ -278,6 +306,7 @@ func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { StepDefinition: formatters.StepDefinition{ Handler: stepFunc, Expr: regex, + Keyword: keyword, }, HandlerValue: v, }