diff --git a/arguments.go b/arguments.go index a79b453..afa11b1 100644 --- a/arguments.go +++ b/arguments.go @@ -13,6 +13,13 @@ type Arg struct { value interface{} } +// StepArgument func creates a step argument. +// used in cases when calling another step from +// within a StepHandlerFunc +func StepArgument(value interface{}) *Arg { + return &Arg{value: value} +} + // Float64 converts an argument to float64 // or panics if unable to convert it func (a *Arg) Float64() float64 { diff --git a/events.go b/events.go new file mode 100644 index 0000000..776407b --- /dev/null +++ b/events.go @@ -0,0 +1,103 @@ +package godog + +import "github.com/DATA-DOG/godog/gherkin" + +// BeforeSuiteHandler can be registered +// in Suite to be executed once before +// running a feature suite +type BeforeSuiteHandler interface { + HandleBeforeSuite() +} + +// BeforeSuiteHandlerFunc is a function implementing +// BeforeSuiteHandler interface +type BeforeSuiteHandlerFunc func() + +// HandleBeforeSuite is called once before suite +func (f BeforeSuiteHandlerFunc) HandleBeforeSuite() { + f() +} + +// BeforeScenarioHandler can be registered +// in Suite to be executed before every scenario +// which will be run +type BeforeScenarioHandler interface { + HandleBeforeScenario(scenario *gherkin.Scenario) +} + +// BeforeScenarioHandlerFunc is a function implementing +// BeforeScenarioHandler interface +type BeforeScenarioHandlerFunc func(scenario *gherkin.Scenario) + +// HandleBeforeScenario is called with a *gherkin.Scenario argument +// for before every scenario which is run by suite +func (f BeforeScenarioHandlerFunc) HandleBeforeScenario(scenario *gherkin.Scenario) { + f(scenario) +} + +// BeforeStepHandler can be registered +// in Suite to be executed before every step +// which will be run +type BeforeStepHandler interface { + HandleBeforeStep(step *gherkin.Step) +} + +// BeforeStepHandlerFunc is a function implementing +// BeforeStepHandler interface +type BeforeStepHandlerFunc func(step *gherkin.Step) + +// HandleBeforeStep is called with a *gherkin.Step argument +// for before every step which is run by suite +func (f BeforeStepHandlerFunc) HandleBeforeStep(step *gherkin.Step) { + f(step) +} + +// AfterStepHandler can be registered +// in Suite to be executed after every step +// which will be run +type AfterStepHandler interface { + HandleAfterStep(step *gherkin.Step, status Status) +} + +// AfterStepHandlerFunc is a function implementing +// AfterStepHandler interface +type AfterStepHandlerFunc func(step *gherkin.Step, status Status) + +// HandleAfterStep is called with a *gherkin.Step argument +// for after every step which is run by suite +func (f AfterStepHandlerFunc) HandleAfterStep(step *gherkin.Step, status Status) { + f(step, status) +} + +// AfterScenarioHandler can be registered +// in Suite to be executed after every scenario +// which will be run +type AfterScenarioHandler interface { + HandleAfterScenario(scenario *gherkin.Scenario, status Status) +} + +// AfterScenarioHandlerFunc is a function implementing +// AfterScenarioHandler interface +type AfterScenarioHandlerFunc func(scenario *gherkin.Scenario, status Status) + +// HandleAfterScenario is called with a *gherkin.Scenario argument +// for after every scenario which is run by suite +func (f AfterScenarioHandlerFunc) HandleAfterScenario(scenario *gherkin.Scenario, status Status) { + f(scenario, status) +} + +// AfterSuiteHandler can be registered +// in Suite to be executed once after +// running a feature suite +type AfterSuiteHandler interface { + HandleAfterSuite() +} + +// AfterSuiteHandlerFunc is a function implementing +// AfterSuiteHandler interface +type AfterSuiteHandlerFunc func() + +// HandleAfterSuite is called once after suite +func (f AfterSuiteHandlerFunc) HandleAfterSuite() { + f() +} diff --git a/features/hooks.feature b/features/hooks.feature index 1022d14..581f0ec 100644 --- a/features/hooks.feature +++ b/features/hooks.feature @@ -4,10 +4,9 @@ Feature: suite hooks I need to provide a way to hook into these events Background: - Given I have a before scenario hook - And a feature path "features/load_features.feature:6" - And I parse features + Given I'm listening to suite events Scenario: triggers before scenario hook - When I run features - Then I should have a scenario "load features within path" recorded in the hook + Given a feature path "features/load_features.feature:6" + When I run feature suite + Then there was event triggered before scenario "load features within path" diff --git a/suite.go b/suite.go index d4f03a6..ea2a973 100644 --- a/suite.go +++ b/suite.go @@ -11,22 +11,27 @@ import ( "github.com/DATA-DOG/godog/gherkin" ) -type stepsStatus int +// Status represents a step status +type Status int const ( - stepsStatusPassed stepsStatus = iota - stepsStatusFailed - stepsStatusUndefined + invalid Status = iota + Passed + Failed + Undefined ) -type BeforeScenarioHandler interface { - BeforeScenario(scenario *gherkin.Scenario) -} - -type BeforeScenarioHandlerFunc func(scenario *gherkin.Scenario) - -func (f BeforeScenarioHandlerFunc) BeforeScenario(scenario *gherkin.Scenario) { - f(scenario) +// String represents status as string +func (s Status) String() string { + switch s { + case Passed: + return "passed" + case Failed: + return "failed" + case Undefined: + return "undefined" + } + return "invalid" } // Objects implementing the StepHandler interface can be @@ -66,16 +71,29 @@ type stepMatchHandler struct { // to register step definitions and event handlers type Suite interface { Step(expr *regexp.Regexp, h StepHandler) + // suite events + BeforeSuite(h BeforeSuiteHandler) BeforeScenario(h BeforeScenarioHandler) + BeforeStep(h BeforeStepHandler) + AfterStep(h AfterStepHandler) + AfterScenario(h AfterScenarioHandler) + AfterSuite(h AfterSuiteHandler) } type suite struct { - beforeScenarioHandlers []BeforeScenarioHandler - stepHandlers []*stepMatchHandler - features []*gherkin.Feature - fmt Formatter + stepHandlers []*stepMatchHandler + features []*gherkin.Feature + fmt Formatter failed bool + + // suite event handlers + beforeSuiteHandlers []BeforeSuiteHandler + beforeScenarioHandlers []BeforeScenarioHandler + beforeStepHandlers []BeforeStepHandler + afterStepHandlers []AfterStepHandler + afterScenarioHandlers []AfterScenarioHandler + afterSuiteHandlers []AfterSuiteHandler } // New initializes a suite which supports the Suite @@ -102,10 +120,42 @@ func (s *suite) Step(expr *regexp.Regexp, h StepHandler) { }) } +// BeforeSuite registers a BeforeSuiteHandler +// to be run once before suite runner +func (s *suite) BeforeSuite(h BeforeSuiteHandler) { + s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, h) +} + +// BeforeScenario registers a BeforeScenarioHandler +// to be run before every scenario func (s *suite) BeforeScenario(h BeforeScenarioHandler) { s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, h) } +// BeforeStep registers a BeforeStepHandler +// to be run before every scenario +func (s *suite) BeforeStep(h BeforeStepHandler) { + s.beforeStepHandlers = append(s.beforeStepHandlers, h) +} + +// AfterStep registers an AfterStepHandler +// to be run after every scenario +func (s *suite) AfterStep(h AfterStepHandler) { + s.afterStepHandlers = append(s.afterStepHandlers, h) +} + +// AfterScenario registers an AfterScenarioHandler +// to be run after every scenario +func (s *suite) AfterScenario(h AfterScenarioHandler) { + s.afterScenarioHandlers = append(s.afterScenarioHandlers, h) +} + +// AfterSuite registers a AfterSuiteHandler +// to be run once after suite runner +func (s *suite) AfterSuite(h AfterSuiteHandler) { + s.afterSuiteHandlers = append(s.afterSuiteHandlers, h) +} + // Run - runs a godog feature suite func (s *suite) Run() { var err error @@ -129,6 +179,19 @@ func (s *suite) Run() { s.features, err = cfg.features() fatal(err) + s.run() + + if s.failed { + os.Exit(1) + } +} + +func (s *suite) run() { + // run before suite handlers + for _, h := range s.beforeSuiteHandlers { + h.HandleBeforeSuite() + } + // run features for _, f := range s.features { s.runFeature(f) if s.failed && cfg.stopOnFailure { @@ -136,10 +199,11 @@ func (s *suite) Run() { break } } - s.fmt.Summary() - if s.failed { - os.Exit(1) + // run after suite handlers + for _, h := range s.afterSuiteHandlers { + h.HandleAfterSuite() } + s.fmt.Summary() } func (s *suite) runStep(step *gherkin.Step) (err error) { @@ -180,18 +244,31 @@ func (s *suite) runStep(step *gherkin.Step) (err error) { return } -func (s *suite) runSteps(steps []*gherkin.Step) (st stepsStatus) { +func (s *suite) runSteps(steps []*gherkin.Step) (st Status) { for _, step := range steps { - if st != stepsStatusPassed { + if st == Failed || st == Undefined { s.fmt.Skipped(step) continue } + + // run before step handlers + for _, h := range s.beforeStepHandlers { + h.HandleBeforeStep(step) + } + err := s.runStep(step) - switch { - case err == errPending: - st = stepsStatusUndefined - case err != nil: - st = stepsStatusFailed + switch err { + case errPending: + st = Undefined + case nil: + st = Passed + default: + st = Failed + } + + // run after step handlers + for _, h := range s.afterStepHandlers { + h.HandleAfterStep(step, st) } } return @@ -206,11 +283,11 @@ func (s *suite) skipSteps(steps []*gherkin.Step) { func (s *suite) runFeature(f *gherkin.Feature) { s.fmt.Node(f) for _, scenario := range f.Scenarios { - var status stepsStatus + var status Status // run before scenario handlers for _, h := range s.beforeScenarioHandlers { - h.BeforeScenario(scenario) + h.HandleBeforeScenario(scenario) } // background @@ -222,15 +299,20 @@ func (s *suite) runFeature(f *gherkin.Feature) { // scenario s.fmt.Node(scenario) switch { - case status == stepsStatusFailed: + case status == Failed: s.skipSteps(scenario.Steps) - case status == stepsStatusUndefined: + case status == Undefined: s.skipSteps(scenario.Steps) - default: + case status == invalid: status = s.runSteps(scenario.Steps) } - if status == stepsStatusFailed { + // run after scenario handlers + for _, h := range s.afterScenarioHandlers { + h.HandleAfterScenario(scenario, status) + } + + if status == Failed { s.failed = true if cfg.stopOnFailure { return diff --git a/suite_test.go b/suite_test.go index 23fec0a..b33bf7c 100644 --- a/suite_test.go +++ b/suite_test.go @@ -3,28 +3,35 @@ package godog import ( "fmt" "regexp" + "strings" "github.com/DATA-DOG/godog/gherkin" ) -type suiteFeature struct { - suite - // for hook tests - befScenarioHook *gherkin.Scenario +type firedEvent struct { + name string + args []interface{} } -func (s *suiteFeature) BeforeScenario(scenario *gherkin.Scenario) { +type suiteFeature struct { + suite + events []*firedEvent +} + +func (s *suiteFeature) HandleBeforeScenario(scenario *gherkin.Scenario) { // reset feature paths cfg.paths = []string{} - // reset hook test references - s.befScenarioHook = nil + // reset event stack + s.events = []*firedEvent{} // reset formatter, which collects all details s.fmt = &testFormatter{} } -func (s *suiteFeature) iHaveBeforeScenarioHook(args ...*Arg) error { - s.suite.BeforeScenario(BeforeScenarioHandlerFunc(func(scenario *gherkin.Scenario) { - s.befScenarioHook = scenario +func (s *suiteFeature) iAmListeningToSuiteEvents(args ...*Arg) error { + s.BeforeScenario(BeforeScenarioHandlerFunc(func(scenario *gherkin.Scenario) { + s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{ + scenario, + }}) })) return nil } @@ -59,10 +66,11 @@ func (s *suiteFeature) iShouldHaveNumFeatureFiles(args ...*Arg) error { return nil } -func (s *suiteFeature) iRunFeatures(args ...*Arg) error { - for _, f := range s.features { - s.runFeature(f) +func (s *suiteFeature) iRunFeatureSuite(args ...*Arg) error { + if err := s.parseFeatures(); err != nil { + return err } + s.run() return nil } @@ -77,14 +85,39 @@ func (s *suiteFeature) numScenariosRegistered(args ...*Arg) (err error) { return } -func (s *suiteFeature) iShouldHaveScenarioRecordedInHook(args ...*Arg) (err error) { - if s.befScenarioHook == nil { - return fmt.Errorf("there was no scenario executed in before hook") +func (s *suiteFeature) thereWereNumEventsFired(args ...*Arg) error { + var num int + for _, event := range s.events { + if event.name == args[2].String() { + num++ + } } - if s.befScenarioHook.Title != args[0].String() { - err = fmt.Errorf(`expected "%s" scenario to be run in hook, but got "%s"`, args[0].String(), s.befScenarioHook.Title) + if num != args[1].Int() { + return fmt.Errorf("expected %d %s events to be fired, but got %d", args[1].Int(), args[2].String(), num) } - return + return nil +} + +func (s *suiteFeature) thereWasEventTriggeredBeforeScenario(args ...*Arg) error { + var found []string + for _, event := range s.events { + if event.name != "BeforeScenario" { + continue + } + + scenario := event.args[0].(*gherkin.Scenario) + if scenario.Title == args[0].String() { + return nil + } + + found = append(found, scenario.Title) + } + + if len(found) == 0 { + return fmt.Errorf("before scenario event was never triggered or listened") + } + + return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, args[0].String(), `"`+strings.Join(found, `", "`)+`"`) } func SuiteContext(g Suite) { @@ -107,12 +140,15 @@ func SuiteContext(g Suite) { regexp.MustCompile(`^I should have ([\d]+) scenarios? registered$`), StepHandlerFunc(s.numScenariosRegistered)) g.Step( - regexp.MustCompile(`^I have a before scenario hook$`), - StepHandlerFunc(s.iHaveBeforeScenarioHook)) + regexp.MustCompile(`^I'm listening to suite events$`), + StepHandlerFunc(s.iAmListeningToSuiteEvents)) g.Step( - regexp.MustCompile(`^I run features$`), - StepHandlerFunc(s.iRunFeatures)) + regexp.MustCompile(`^I run feature suite$`), + StepHandlerFunc(s.iRunFeatureSuite)) g.Step( - regexp.MustCompile(`^I should have a scenario "([^"]*)" recorded in the hook$`), - StepHandlerFunc(s.iShouldHaveScenarioRecordedInHook)) + regexp.MustCompile(`^there (was|were) ([\d]+) "([^"]*)" events? fired$`), + StepHandlerFunc(s.thereWereNumEventsFired)) + g.Step( + regexp.MustCompile(`^there was event triggered before scenario "([^"]*)"$`), + StepHandlerFunc(s.thereWasEventTriggeredBeforeScenario)) }