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
Этот коммит содержится в:
Alexandru Chitu 2023-01-23 14:22:19 +00:00 коммит произвёл GitHub
родитель c035051d94
коммит 62d84eedbc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 289 добавлений и 26 удалений

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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
"""

Просмотреть файл

@ -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
"""

Просмотреть файл

@ -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
)

Просмотреть файл

@ -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
`

Просмотреть файл

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

Просмотреть файл

@ -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"}
})

Просмотреть файл

@ -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,
}