From 703b40de76ce6dfd0c25588071aeada1ae22698b Mon Sep 17 00:00:00 2001 From: gedi Date: Thu, 27 Apr 2017 23:48:54 +0300 Subject: [PATCH] adds scenarios to test nested steps --- features/lang.feature | 3 +- features/load.feature | 3 +- features/multistep.feature | 140 +++++++++++++++++++++++++++++++++++++ run.go | 6 +- stepdef.go | 17 +++++ suite.go | 104 +++++++++++++++------------ suite_test.go | 15 ++++ 7 files changed, 236 insertions(+), 52 deletions(-) create mode 100644 features/multistep.feature diff --git a/features/lang.feature b/features/lang.feature index 829a939..594786e 100644 --- a/features/lang.feature +++ b/features/lang.feature @@ -8,7 +8,7 @@ Savybė: užkrauti savybes Scenarijus: savybių užkrovimas iš aplanko Duota savybių aplankas "features" Kai aš išskaitau savybes - Tada aš turėčiau turėti 9 savybių failus: + Tada aš turėčiau turėti 10 savybių failus: """ features/background.feature features/events.feature @@ -16,6 +16,7 @@ Savybė: užkrauti savybes features/formatter/events.feature features/lang.feature features/load.feature + features/multistep.feature features/outline.feature features/run.feature features/snippets.feature diff --git a/features/load.feature b/features/load.feature index 22ed683..7b64dec 100644 --- a/features/load.feature +++ b/features/load.feature @@ -6,7 +6,7 @@ Feature: load features Scenario: load features within path Given a feature path "features" When I parse features - Then I should have 9 feature files: + Then I should have 10 feature files: """ features/background.feature features/events.feature @@ -14,6 +14,7 @@ Feature: load features features/formatter/events.feature features/lang.feature features/load.feature + features/multistep.feature features/outline.feature features/run.feature features/snippets.feature diff --git a/features/multistep.feature b/features/multistep.feature new file mode 100644 index 0000000..95885dd --- /dev/null +++ b/features/multistep.feature @@ -0,0 +1,140 @@ +Feature: run features with nested steps + In order to test multisteps + As a test suite + I need to be able to execute multisteps + + Scenario: should run passing multistep successfully + Given a feature "normal.feature" file: + """ + Feature: normal feature + + Scenario: run passing multistep + Given passing step + Then passing multistep + """ + When I run feature suite + Then the suite should have passed + And the following steps should be passed: + """ + passing step + passing multistep + """ + + Scenario: should fail multistep + Given a feature "failed.feature" file: + """ + Feature: failed feature + + Scenario: run failing multistep + Given passing step + When failing multistep + Then I should have 1 scenario registered + """ + When I run feature suite + Then the suite should have failed + And the following step should be failed: + """ + failing multistep + """ + And the following steps should be skipped: + """ + I should have 1 scenario registered + """ + And the following steps should be passed: + """ + passing step + """ + + Scenario: should fail nested multistep + Given a feature "failed.feature" file: + """ + Feature: failed feature + + Scenario: run failing nested multistep + Given failing nested multistep + When passing step + """ + When I run feature suite + Then the suite should have failed + And the following step should be failed: + """ + failing nested multistep + """ + And the following steps should be skipped: + """ + passing step + """ + + Scenario: should skip steps after undefined multistep + Given a feature "undefined.feature" file: + """ + Feature: run undefined multistep + + Scenario: run undefined multistep + Given passing step + When undefined multistep + Then passing multistep + """ + When I run feature suite + Then the suite should have passed + And the following step should be passed: + """ + passing step + """ + And the following step should be undefined: + """ + undefined multistep + """ + And the following step should be skipped: + """ + passing multistep + """ + + Scenario: should match undefined steps in a row + Given a feature "undefined.feature" file: + """ + Feature: undefined feature + + Scenario: parse a scenario + Given undefined step + When undefined multistep + Then I should have 1 scenario registered + """ + When I run feature suite + Then the suite should have passed + And the following steps should be undefined: + """ + undefined step + undefined multistep + """ + And the following step should be skipped: + """ + I should have 1 scenario registered + """ + + Scenario: should mark undefined steps after pending + Given a feature "pending.feature" file: + """ + Feature: pending feature + + Scenario: parse a scenario + Given pending step + When undefined step + Then undefined multistep + And I should have 1 scenario registered + """ + When I run feature suite + Then the suite should have passed + And the following steps should be undefined: + """ + undefined step + undefined multistep + """ + And the following step should be pending: + """ + pending step + """ + And the following step should be skipped: + """ + I should have 1 scenario registered + """ diff --git a/run.go b/run.go index 965906b..1167744 100644 --- a/run.go +++ b/run.go @@ -160,13 +160,11 @@ func Run(suite string, contextInitializer func(suite *Suite)) int { func supportsConcurrency(format string) bool { switch format { case "events": - return false case "junit": - return false case "pretty": - return false case "cucumber": - return false + default: + return true // supports concurrency } return true // all custom formatters are treated as supporting concurrency diff --git a/stepdef.go b/stepdef.go index 8f9e3ab..6233a17 100644 --- a/stepdef.go +++ b/stepdef.go @@ -14,6 +14,22 @@ import ( var matchFuncDefRef = regexp.MustCompile(`\(([^\)]+)\)`) +// Steps allows to nest steps +// 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), +// } +// } +// +// These steps will be matched and executed in +// sequential order. The first one which fails +// will result in main step failure. +type Steps []string + // StepDef is a registered step definition // contains a StepHandler and regexp which // is used to match a step. Args which @@ -27,6 +43,7 @@ type StepDef struct { hv reflect.Value Expr *regexp.Regexp Handler interface{} + nested bool } func (sd *StepDef) definitionID() string { diff --git a/suite.go b/suite.go index 8066856..a1d7b86 100644 --- a/suite.go +++ b/suite.go @@ -100,6 +100,12 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { panic(fmt.Sprintf("expected handler to return only one value, but it has: %d", typ.NumOut())) } + def := &StepDef{ + Handler: stepFunc, + Expr: regex, + hv: v, + } + typ = typ.Out(0) switch typ.Kind() { case reflect.Interface: @@ -107,6 +113,7 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { panic(fmt.Sprintf("expected handler to return an error, but got: %s", typ.Kind())) } case reflect.Slice: + def.nested = true if typ.Elem().Kind() != reflect.String { panic(fmt.Sprintf("expected handler to return []string for multistep, but got: []%s", typ.Kind())) } @@ -114,11 +121,7 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { panic(fmt.Sprintf("expected handler to return an error or []string, but got: %s", typ.Kind())) } - s.steps = append(s.steps, &StepDef{ - Handler: stepFunc, - Expr: regex, - hv: v, - }) + s.steps = append(s.steps, def) } // BeforeSuite registers a function or method @@ -197,27 +200,20 @@ func (s *Suite) run() { } func (s *Suite) matchStep(step *gherkin.Step) *StepDef { - for _, h := range s.steps { - if m := h.Expr.FindStringSubmatch(step.Text); len(m) > 0 { - var args []interface{} - for _, m := range m[1:] { - args = append(args, m) - } - if step.Argument != nil { - args = append(args, step.Argument) - } - h.args = args - return h - } + def := s.matchStepText(step.Text) + if def != nil && step.Argument != nil { + def.args = append(def.args, step.Argument) } - // @TODO can handle ambiguous - return nil + return def } func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { match := s.matchStep(step) s.fmt.Defined(step, match) - if match == nil { + + // @TODO custom undefined err here to pass step text for snippet + // @TODO user multistep definitions may panic + if s.maybeUndefined(match) { s.fmt.Undefined(step) return ErrUndefined } @@ -227,11 +223,6 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { return nil } - // run before step handlers - for _, f := range s.beforeStepHandlers { - f(step) - } - defer func() { if e := recover(); e != nil { err = &traceError{ @@ -254,10 +245,32 @@ func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { } }() + // run before step handlers + for _, f := range s.beforeStepHandlers { + f(step) + } + err = s.maybeSubSteps(match.run()) return } +func (s *Suite) maybeUndefined(step *StepDef) bool { + if nil == step { + return true + } + + if !step.nested { + return false + } + + for _, text := range step.run().(Steps) { + if s.maybeUndefined(s.matchStepText(text)) { + return true + } + } + return false +} + func (s *Suite) maybeSubSteps(result interface{}) error { if nil == result { return nil @@ -267,38 +280,37 @@ func (s *Suite) maybeSubSteps(result interface{}) error { return err } - steps, ok := result.([]string) + steps, ok := result.(Steps) if !ok { return fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result) } - for _, step := range steps { - var def *StepDef - for _, h := range s.steps { - if m := h.Expr.FindStringSubmatch(step); len(m) > 0 { - var args []interface{} - for _, m := range m[1:] { - args = append(args, m) - } - h.args = args - def = h - break - } - } - - if def == nil { + for _, text := range steps { + if def := s.matchStepText(text); def == nil { return ErrUndefined - } - - // @TODO: step hooks only take gherkin.Step - // @TODO: cannot call formatter to register step execution either - if err := s.maybeSubSteps(def.run()); err != nil { + } else if err := s.maybeSubSteps(def.run()); err != nil { return err } } return nil } +func (s *Suite) matchStepText(text string) *StepDef { + for _, h := range s.steps { + if m := h.Expr.FindStringSubmatch(text); len(m) > 0 { + var args []interface{} + for _, m := range m[1:] { + args = append(args, m) + } + + // @TODO copy step def + h.args = args + return h + } + } + return nil +} + func (s *Suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) { err = prevErr for _, step := range steps { diff --git a/suite_test.go b/suite_test.go index d27174c..90601f3 100644 --- a/suite_test.go +++ b/suite_test.go @@ -89,6 +89,21 @@ func SuiteContext(s *Suite) { // Introduced to test formatter/cucumber.feature s.Step(`^the rendered json will be as follows:$`, c.theRenderJSONWillBe) + s.Step(`^failing multistep$`, func() Steps { + return Steps{"passing step", "failing step"} + }) + + s.Step(`^undefined multistep$`, func() Steps { + return Steps{"passing step", "undefined step", "passing step"} + }) + + s.Step(`^passing multistep$`, func() Steps { + return Steps{"passing step", "passing step", "passing step"} + }) + + s.Step(`^failing nested multistep$`, func() Steps { + return Steps{"passing step", "passing multistep", "failing multistep"} + }) } type firedEvent struct {