diff --git a/README.md b/README.md index a378edb..0ab8c62 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,14 @@ corruption or race conditions in the application. It is also useful to randomize the order of scenario execution, which you can now do with **--random** command option. +**NOTE:** if suite runs with concurrency option, it concurrently runs +every feature, not scenario per different features. This gives +a flexibility to isolate state per feature. For example using +**BeforeFeature** hook, it is possible to spin up costly service and shut +it down only in **AfterFeature** hook and share the service between all +scenarios in that feature. It is not advisable though, because you are +risking having a state dependency. + ## Contributions Feel free to open a pull request. Note, if you wish to contribute an extension to public (exported methods or types) - diff --git a/features/events.feature b/features/events.feature index 1c7bc66..e921aaa 100644 --- a/features/events.feature +++ b/features/events.feature @@ -16,10 +16,12 @@ Feature: suite events When I run feature suite Then these events had to be fired for a number of times: | BeforeSuite | 1 | + | BeforeFeature | 1 | | BeforeScenario | 1 | | BeforeStep | 3 | | AfterStep | 3 | | AfterScenario | 1 | + | AfterFeature | 1 | | AfterSuite | 1 | Scenario: triggers appropriate events whole feature @@ -27,8 +29,25 @@ Feature: suite events When I run feature suite Then these events had to be fired for a number of times: | BeforeSuite | 1 | + | BeforeFeature | 1 | | BeforeScenario | 6 | | BeforeStep | 19 | | AfterStep | 19 | | AfterScenario | 6 | + | AfterFeature | 1 | | AfterSuite | 1 | + + Scenario: triggers appropriate events for two feature files + Given a feature path "features/load.feature:6" + And a feature path "features/multistep.feature:6" + When I run feature suite + Then these events had to be fired for a number of times: + | BeforeSuite | 1 | + | BeforeFeature | 2 | + | BeforeScenario | 2 | + | BeforeStep | 7 | + | AfterStep | 7 | + | AfterScenario | 2 | + | AfterFeature | 2 | + | AfterSuite | 1 | + diff --git a/suite.go b/suite.go index 9f3ad81..a2d0806 100644 --- a/suite.go +++ b/suite.go @@ -55,10 +55,12 @@ type Suite struct { // suite event handlers beforeSuiteHandlers []func() + beforeFeatureHandlers []func(*gherkin.Feature) beforeScenarioHandlers []func(interface{}) beforeStepHandlers []func(*gherkin.Step) afterStepHandlers []func(*gherkin.Step, error) afterScenarioHandlers []func(interface{}, error) + afterFeatureHandlers []func(*gherkin.Feature) afterSuiteHandlers []func() } @@ -130,8 +132,26 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) { // // Use it to prepare the test suite for a spin. // Connect and prepare database for instance... -func (s *Suite) BeforeSuite(f func()) { - s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, f) +func (s *Suite) BeforeSuite(fn func()) { + s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, fn) +} + +// BeforeFeature registers a function or method +// to be run once before every feature execution. +// +// If godog is run with concurrency option, it will +// run every feature per goroutine. So user may choose +// whether to isolate state within feature context or +// scenario. +// +// Best practice is not to have any state dependency on +// every scenario, but in some cases if VM for example +// needs to be started it may take very long for each +// scenario to restart it. +// +// Use it wisely and avoid sharing state between scenarios. +func (s *Suite) BeforeFeature(fn func(*gherkin.Feature)) { + s.beforeFeatureHandlers = append(s.beforeFeatureHandlers, fn) } // BeforeScenario registers a function or method @@ -143,14 +163,14 @@ func (s *Suite) BeforeSuite(f func()) { // It is a good practice to restore the default state // before every scenario so it would be isolated from // any kind of state. -func (s *Suite) BeforeScenario(f func(interface{})) { - s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, f) +func (s *Suite) BeforeScenario(fn func(interface{})) { + s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, fn) } // BeforeStep registers a function or method // to be run before every scenario -func (s *Suite) BeforeStep(f func(*gherkin.Step)) { - s.beforeStepHandlers = append(s.beforeStepHandlers, f) +func (s *Suite) BeforeStep(fn func(*gherkin.Step)) { + s.beforeStepHandlers = append(s.beforeStepHandlers, fn) } // AfterStep registers an function or method @@ -162,8 +182,8 @@ func (s *Suite) BeforeStep(f func(*gherkin.Step)) { // // In some cases, for example when running a headless // browser, to take a screenshot after failure. -func (s *Suite) AfterStep(f func(*gherkin.Step, error)) { - s.afterStepHandlers = append(s.afterStepHandlers, f) +func (s *Suite) AfterStep(fn func(*gherkin.Step, error)) { + s.afterStepHandlers = append(s.afterStepHandlers, fn) } // AfterScenario registers an function or method @@ -171,14 +191,20 @@ func (s *Suite) AfterStep(f func(*gherkin.Step, error)) { // // The interface argument may be *gherkin.Scenario // or *gherkin.ScenarioOutline -func (s *Suite) AfterScenario(f func(interface{}, error)) { - s.afterScenarioHandlers = append(s.afterScenarioHandlers, f) +func (s *Suite) AfterScenario(fn func(interface{}, error)) { + s.afterScenarioHandlers = append(s.afterScenarioHandlers, fn) +} + +// AfterFeature registers a function or method +// to be run once after feature executed all scenarios. +func (s *Suite) AfterFeature(fn func(*gherkin.Feature)) { + s.afterFeatureHandlers = append(s.afterFeatureHandlers, fn) } // AfterSuite registers a function or method // to be run once after suite runner -func (s *Suite) AfterSuite(f func()) { - s.afterSuiteHandlers = append(s.afterSuiteHandlers, f) +func (s *Suite) AfterSuite(fn func()) { + s.afterSuiteHandlers = append(s.afterSuiteHandlers, fn) } func (s *Suite) run() { @@ -492,6 +518,10 @@ func (s *Suite) shouldFail(err error) bool { } func (s *Suite) runFeature(f *feature) { + for _, fn := range s.beforeFeatureHandlers { + fn(f.Feature) + } + s.fmt.Feature(f.Feature, f.Path, f.Content) // make a local copy of the feature scenario defenitions, @@ -507,6 +537,12 @@ func (s *Suite) runFeature(f *feature) { copy(scenarios, f.ScenarioDefinitions) } + defer func() { + for _, fn := range s.afterFeatureHandlers { + fn(f.Feature) + } + }() + for _, scenario := range scenarios { var err error if f.Background != nil { diff --git a/suite_context.go b/suite_context.go index a9b638a..e8528ff 100644 --- a/suite_context.go +++ b/suite_context.go @@ -275,6 +275,12 @@ func (s *suiteContext) iAmListeningToSuiteEvents() error { s.testedSuite.AfterSuite(func() { s.events = append(s.events, &firedEvent{"AfterSuite", []interface{}{}}) }) + s.testedSuite.BeforeFeature(func(ft *gherkin.Feature) { + s.events = append(s.events, &firedEvent{"BeforeFeature", []interface{}{ft}}) + }) + s.testedSuite.AfterFeature(func(ft *gherkin.Feature) { + s.events = append(s.events, &firedEvent{"AfterFeature", []interface{}{ft}}) + }) s.testedSuite.BeforeScenario(func(scenario interface{}) { s.events = append(s.events, &firedEvent{"BeforeScenario", []interface{}{scenario}}) })