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
Этот коммит содержится в:
родитель
c035051d94
коммит
62d84eedbc
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
|
||||
)
|
||||
|
|
14
run_test.go
14
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
|
||||
`
|
||||
|
||||
|
|
35
suite.go
35
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
|
||||
|
|
|
@ -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"}
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче