diff --git a/README.md b/README.md index 0aa516d..dffa6ab 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,15 @@ not changed most likely. I'll try to respect **backward compatibility** as much ### Example +The following example can be [found here](https://github.com/DATA-DOG/godog/tree/master/examples/godogs). + +#### Step 1 + Imagine we have a **godog cart** to serve godogs for dinner. At first, we describe our feature in plain text: ``` gherkin -# file: /tmp/godog/godog.feature +# file: examples/godogs/godog.feature Feature: eat godogs In order to be happy As a hungry gopher @@ -44,22 +48,27 @@ Feature: eat godogs As a developer, your work is done as soon as you’ve made the program behave as described in the Scenario. -If you run `godog godog.feature` inside the **/tmp/godog** directory. +#### Step 2 + +If you run `godog godog.feature` inside the **examples/godogs** directory. You should see that the steps are undefined: ![Screenshot](https://raw.github.com/DATA-DOG/godog/master/screenshots/undefined.png) It gives you undefined step snippets to implement in your test context. You may copy these snippets -into **godog_test.go** file. +into your `*_test.go` file. Now if you run the tests again. You should see that the definition is now pending. You may change **ErrPending** to **nil** and the scenario will pass successfully. -Since we need a working implementation, we may start by implementing what is necessary. -We only need a number of **godogs** for now. +Since we need a working implementation, we may start by implementing only what is necessary. + +#### Step 3 + +We only need a number of **godogs** for now. Lets define steps. ``` go -/* file: /tmp/godog/godog.go */ +/* file: examples/godogs/godog.go */ package main var Godogs int @@ -67,10 +76,12 @@ var Godogs int func main() { /* usual main func */ } ``` +#### Step 4 + Now lets finish our step implementations in order to test our feature requirements: ``` go -/* file: /tmp/godog/godog_test.go */ +/* file: examples/godogs/godog_test.go */ package main import ( @@ -99,7 +110,7 @@ func thereShouldBeRemaining(remaining int) error { return nil } -func featureContext(s godog.Suite) { +func featureContext(s *godog.Suite) { s.Step(`^there are (\d+) godogs$`, thereAreGodogs) s.Step(`^I eat (\d+)$`, iEat) s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) @@ -126,8 +137,15 @@ See `godog -h` for general command options. See implementation examples: -- [rest API server](https://github.com/DATA-DOG/godog/tree/master/examples/api) implementation and tests -- [ls command](https://github.com/DATA-DOG/godog/tree/master/examples/ls) implementation and tests +- [rest API server](https://github.com/DATA-DOG/godog/tree/master/examples/api) +- [godogs](https://github.com/DATA-DOG/godog/tree/master/examples/godogs) + +### Changes + +**2015-07-03** +- changed **godog.Suite** from interface to struct. Context registration should be updated accordingly. The reason +for change: since it exports the same methods and there is no need to mock a function in tests, there is no +obvious reason to keep an interface. ### FAQ diff --git a/builder.go b/builder.go index 722d0cd..4d3f606 100644 --- a/builder.go +++ b/builder.go @@ -82,7 +82,7 @@ func (b *builder) parseFile(path string) error { b.Internal = true } b.deleteMainFunc(f) - b.registerSteps(f) + b.registerContexts(f) b.deleteImports(f) b.files[path] = f @@ -122,22 +122,25 @@ func (b *builder) deleteMainFunc(f *ast.File) { f.Decls = decls } -func (b *builder) registerSteps(f *ast.File) { +func (b *builder) registerContexts(f *ast.File) { for _, d := range f.Decls { switch fun := d.(type) { case *ast.FuncDecl: for _, param := range fun.Type.Params.List { switch expr := param.Type.(type) { - case *ast.SelectorExpr: + case *ast.StarExpr: switch x := expr.X.(type) { case *ast.Ident: - if x.Name == "godog" && expr.Sel.Name == "Suite" { + if x.Name == "Suite" { b.Contexts = append(b.Contexts, fun.Name.Name) } - } - case *ast.Ident: - if expr.Name == "Suite" { - b.Contexts = append(b.Contexts, fun.Name.Name) + case *ast.SelectorExpr: + switch t := x.X.(type) { + case *ast.Ident: + if t.Name == "godog" && x.Sel.Name == "Suite" { + b.Contexts = append(b.Contexts, fun.Name.Name) + } + } } } } diff --git a/examples/api/README.md b/examples/api/README.md index ab16da6..1abb8f4 100644 --- a/examples/api/README.md +++ b/examples/api/README.md @@ -75,7 +75,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) error { return godog.ErrPending } -func featureContext(s godog.Suite) { +func featureContext(s *godog.Suite) { api := &apiFeature{} s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, api.iSendrequestTo) s.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe) @@ -158,7 +158,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) (err er return } -func featureContext(s godog.Suite) { +func featureContext(s *godog.Suite) { api := &apiFeature{} s.BeforeScenario(api.resetResponse) diff --git a/examples/api/api_test.go b/examples/api/api_test.go index 7c34851..a96d03a 100644 --- a/examples/api/api_test.go +++ b/examples/api/api_test.go @@ -67,7 +67,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) (err er return } -func featureContext(s godog.Suite) { +func featureContext(s *godog.Suite) { api := &apiFeature{} s.BeforeScenario(api.resetResponse) diff --git a/examples/api/screenshots/undefined.png b/examples/api/screenshots/undefined.png index ffd2c81..3d7ae77 100644 Binary files a/examples/api/screenshots/undefined.png and b/examples/api/screenshots/undefined.png differ diff --git a/examples/db/api_test.go b/examples/db/api_test.go index 441af17..4ac359b 100644 --- a/examples/db/api_test.go +++ b/examples/db/api_test.go @@ -117,7 +117,7 @@ func (a *apiFeature) thereAreUsers(users *gherkin.DataTable) error { return nil } -func featureContext(s godog.Suite) { +func featureContext(s *godog.Suite) { api := &apiFeature{} s.BeforeScenario(api.resetResponse) diff --git a/examples/godogs/godog.feature b/examples/godogs/godog.feature new file mode 100644 index 0000000..9e24620 --- /dev/null +++ b/examples/godogs/godog.feature @@ -0,0 +1,9 @@ +Feature: eat godogs + In order to be happy + As a hungry gopher + I need to be able to eat godogs + + Scenario: Eat 5 out of 12 + Given there are 12 godogs + When I eat 5 + Then there should be 7 remaining diff --git a/examples/godogs/godog.go b/examples/godogs/godog.go new file mode 100644 index 0000000..69b51b2 --- /dev/null +++ b/examples/godogs/godog.go @@ -0,0 +1,5 @@ +package main + +var Godogs int + +func main() { /* usual main func */ } diff --git a/examples/godogs/godog_test.go b/examples/godogs/godog_test.go new file mode 100644 index 0000000..b94f2dc --- /dev/null +++ b/examples/godogs/godog_test.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + + "github.com/DATA-DOG/godog" +) + +func thereAreGodogs(available int) error { + Godogs = available + return nil +} + +func iEat(num int) error { + if Godogs < num { + return fmt.Errorf("you cannot eat %d godogs, there are %d available", num, Godogs) + } + Godogs -= num + return nil +} + +func thereShouldBeRemaining(remaining int) error { + if Godogs != remaining { + return fmt.Errorf("expected %d godogs to be remaining, but there is %d", remaining, Godogs) + } + return nil +} + +func featureContext(s *godog.Suite) { + s.Step(`^there are (\d+) godogs$`, thereAreGodogs) + s.Step(`^I eat (\d+)$`, iEat) + s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) + + s.BeforeScenario(func(interface{}) { + Godogs = 0 // clean the state before every scenario + }) +} diff --git a/examples/ls/README.md b/examples/ls/README.md deleted file mode 100644 index ca39a97..0000000 --- a/examples/ls/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# ls feature - -In order to test our **ls** feature with **Godog**, run: - - go get github.com/DATA-DOG/godog/cmd/godog - $GOPATH/bin/godog ls.feature - diff --git a/examples/ls/ls.feature b/examples/ls/ls.feature deleted file mode 100644 index 59780dc..0000000 --- a/examples/ls/ls.feature +++ /dev/null @@ -1,27 +0,0 @@ -Feature: ls - In order to see the directory structure - As a UNIX user - I need to be able to list directory contents - - Background: - Given I am in a directory "test" - - Scenario: lists files in directory - Given I have a file named "foo" - And I have a file named "bar" - When I run ls - Then I should get output: - """ - bar - foo - """ - - Scenario: lists files and directories - Given I have a file named "foo" - And I have a directory named "dir" - When I run ls - Then I should get output: - """ - dir - foo - """ diff --git a/examples/ls/ls.go b/examples/ls/ls.go deleted file mode 100644 index d5549dd..0000000 --- a/examples/ls/ls.go +++ /dev/null @@ -1,35 +0,0 @@ -// Example - demonstrates ls command implementation tests. -package main - -import ( - "io" - "log" - "os" - "path/filepath" -) - -func main() { - var location string - switch { - case os.Args[1] != "": - location = os.Args[1] - default: - location = "." - } - if err := ls(location, os.Stdout); err != nil { - log.Fatal(err) - } -} - -func ls(path string, w io.Writer) error { - return filepath.Walk(path, func(p string, f os.FileInfo, err error) error { - switch { - case f.IsDir() && f.Name() != "." && f.Name() != ".." && filepath.Base(path) != f.Name(): - w.Write([]byte(f.Name() + "\n")) - return filepath.SkipDir - case !f.IsDir(): - w.Write([]byte(f.Name() + "\n")) - } - return err - }) -} diff --git a/examples/ls/ls_test.go b/examples/ls/ls_test.go deleted file mode 100644 index 60b2181..0000000 --- a/examples/ls/ls_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "io/ioutil" - "os" - "strings" - - "github.com/DATA-DOG/godog" - "github.com/cucumber/gherkin-go" -) - -type lsFeature struct { - dir string - buf *bytes.Buffer -} - -func lsFeatureContext(s godog.Suite) { - c := &lsFeature{buf: bytes.NewBuffer(make([]byte, 1024))} - - s.Step(`^I am in a directory "([^"]*)"$`, c.iAmInDirectory) - s.Step(`^I have a (file|directory) named "([^"]*)"$`, c.iHaveFileOrDirectoryNamed) - s.Step(`^I run ls$`, c.iRunLs) - s.Step(`^I should get output:$`, c.iShouldGetOutput) -} - -func (f *lsFeature) iAmInDirectory(name string) error { - f.dir = os.TempDir() + "/" + name - if err := os.RemoveAll(f.dir); err != nil && !os.IsNotExist(err) { - return err - } - return os.Mkdir(f.dir, 0775) -} - -func (f *lsFeature) iHaveFileOrDirectoryNamed(typ, name string) (err error) { - if len(f.dir) == 0 { - return fmt.Errorf("the directory was not chosen yet") - } - switch typ { - case "file": - err = ioutil.WriteFile(f.dir+"/"+name, []byte{}, 0664) - case "directory": - err = os.Mkdir(f.dir+"/"+name, 0775) - } - return err -} - -func (f *lsFeature) iShouldGetOutput(names *gherkin.DocString) error { - expected := strings.Split(names.Content, "\n") - actual := strings.Split(strings.TrimSpace(f.buf.String()), "\n") - if len(expected) != len(actual) { - return fmt.Errorf("number of expected output lines %d, does not match actual: %d", len(expected), len(actual)) - } - for i, line := range actual { - if line != expected[i] { - return fmt.Errorf(`expected line "%s" at position: %d to match "%s", but it did not`, expected[i], i, line) - } - } - return nil -} - -func (f *lsFeature) iRunLs() error { - f.buf.Reset() - return ls(f.dir, f.buf) -} diff --git a/flags.go b/flags.go index e93567c..8f0c43c 100644 --- a/flags.go +++ b/flags.go @@ -5,7 +5,9 @@ import ( "fmt" ) -func flags(s *suite) *flag.FlagSet { +// Flags builds a *flag.FlagSet with all flags +// required for the godog suite +func flags(s *Suite) *flag.FlagSet { set := flag.NewFlagSet("godog", flag.ExitOnError) set.StringVar(&s.format, "format", "pretty", "") set.StringVar(&s.format, "f", "pretty", "") diff --git a/fmt.go b/fmt.go index ec958b2..d7c086a 100644 --- a/fmt.go +++ b/fmt.go @@ -30,7 +30,7 @@ var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetH return godog.ErrPending } -{{end}}func featureContext(s godog.Suite) { {{ range . }} +{{end}}func featureContext(s *godog.Suite) { {{ range . }} s.Step({{ backticked .Expr }}, {{ .Method }}){{end}} } `)) diff --git a/screenshots/undefined.png b/screenshots/undefined.png index a26cfb2..c8b92eb 100644 Binary files a/screenshots/undefined.png and b/screenshots/undefined.png differ diff --git a/suite.go b/suite.go index a7a0cbf..3a86a56 100644 --- a/suite.go +++ b/suite.go @@ -28,52 +28,18 @@ var ErrUndefined = fmt.Errorf("step is undefined") // step implementation is pending var ErrPending = fmt.Errorf("step implementation is pending") -// Suite is an interface which allows various contexts +// Suite allows various contexts // to register steps and event handlers. // -// When running a test suite, this interface is passed -// to all functions (contexts), which have it as a -// first and only argument. +// When running a test suite, the instance of Suite +// is passed to all functions (contexts), which +// have it as a first and only argument. // // Note that all event hooks does not catch panic errors // in order to have a trace information. Only step // executions are catching panic error since it may // be a context specific error. -type Suite interface { - // Run the test suite - Run() - - // Registers a step which will execute stepFunc - // on step expr match - // - // expr can be either a string or a *regexp.Regexp - // stepFunc is a func to handle the step, arguments - // are set from matched step - Step(expr interface{}, h interface{}) - - // BeforeSuite registers a func to run on initial - // suite startup - BeforeSuite(f func()) - - // BeforeScenario registers a func to run before - // every *gherkin.Scenario or *gherkin.ScenarioOutline - BeforeScenario(f func(interface{})) - - // BeforeStep register a handler before every step - BeforeStep(f func(*gherkin.Step)) - - // AfterStep register a handler after every step - AfterStep(f func(*gherkin.Step, error)) - - // AfterScenario registers a func to run after - // every *gherkin.Scenario or *gherkin.ScenarioOutline - AfterScenario(f func(interface{}, error)) - - // AfterSuite runs func int the end of tests - AfterSuite(f func()) -} - -type suite struct { +type Suite struct { steps []*StepDef features []*feature fmt Formatter @@ -98,24 +64,27 @@ type suite struct { } // New initializes a Suite. The instance is passed around -// to all context initialization functions from *_test.go files -func New() Suite { - return &suite{} +// to all context initialization functions from *_test.go files. +func New() *Suite { + return &Suite{} } -// Step allows to register a StepHandler in Godog -// feature suite, the handler will be applied to all -// steps matching the given Regexp expr +// Step allows to register a *StepDef in Godog +// feature suite, the definition will be applied +// to all steps matching the given Regexp expr. // -// It will panic if expr is not a valid regular expression +// It will panic if expr is not a valid regular +// expression or stepFunc is not a valid step +// handler. // -// Note that if there are two handlers which may match -// the same step, then the only first matched handler +// Note that if there are two definitions which may match +// the same step, then only the first matched handler // will be applied. // -// If none of the StepHandlers are matched, then -// ErrUndefined error will be returned. -func (s *suite) Step(expr interface{}, stepFunc interface{}) { +// If none of the *StepDef is matched, then +// ErrUndefined error will be returned when +// running steps. +func (s *Suite) Step(expr interface{}, stepFunc interface{}) { var regex *regexp.Regexp switch t := expr.(type) { @@ -152,7 +121,7 @@ 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()) { +func (s *Suite) BeforeSuite(f func()) { s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, f) } @@ -165,13 +134,13 @@ 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{})) { +func (s *Suite) BeforeScenario(f func(interface{})) { s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, f) } // BeforeStep registers a function or method // to be run before every scenario -func (s *suite) BeforeStep(f func(*gherkin.Step)) { +func (s *Suite) BeforeStep(f func(*gherkin.Step)) { s.beforeStepHandlers = append(s.beforeStepHandlers, f) } @@ -184,7 +153,7 @@ 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)) { +func (s *Suite) AfterStep(f func(*gherkin.Step, error)) { s.afterStepHandlers = append(s.afterStepHandlers, f) } @@ -193,18 +162,18 @@ 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)) { +func (s *Suite) AfterScenario(f func(interface{}, error)) { s.afterScenarioHandlers = append(s.afterScenarioHandlers, f) } // AfterSuite registers a function or method // to be run once after suite runner -func (s *suite) AfterSuite(f func()) { +func (s *Suite) AfterSuite(f func()) { s.afterSuiteHandlers = append(s.afterSuiteHandlers, f) } // Run starts the Godog feature suite -func (s *suite) Run() { +func (s *Suite) Run() { flagSet := flags(s) fatal(flagSet.Parse(os.Args[1:])) @@ -249,7 +218,7 @@ func (s *suite) Run() { } } -func (s *suite) run() { +func (s *Suite) run() { // run before suite handlers for _, f := range s.beforeSuiteHandlers { f() @@ -269,7 +238,7 @@ func (s *suite) run() { s.fmt.Summary() } -func (s *suite) matchStep(step *gherkin.Step) *StepDef { +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{} @@ -286,7 +255,7 @@ func (s *suite) matchStep(step *gherkin.Step) *StepDef { return nil } -func (s *suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { +func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { match := s.matchStep(step) if match == nil { s.fmt.Undefined(step) @@ -330,7 +299,7 @@ func (s *suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { return } -func (s *suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) { +func (s *Suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) { err = prevErr for _, step := range steps { stepErr := s.runStep(step, err) @@ -347,13 +316,13 @@ func (s *suite) runSteps(steps []*gherkin.Step, prevErr error) (err error) { return } -func (s *suite) skipSteps(steps []*gherkin.Step) { +func (s *Suite) skipSteps(steps []*gherkin.Step) { for _, step := range steps { s.fmt.Skipped(step) } } -func (s *suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Background) (failErr error) { +func (s *Suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Background) (failErr error) { s.fmt.Node(outline) for _, example := range outline.Examples { @@ -403,7 +372,7 @@ func (s *suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Backgrou return } -func (s *suite) runFeature(f *feature) { +func (s *Suite) runFeature(f *feature) { s.fmt.Feature(f.Feature, f.Path) for _, scenario := range f.ScenarioDefinitions { var err error @@ -425,7 +394,7 @@ func (s *suite) runFeature(f *feature) { } } -func (s *suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) (err error) { +func (s *Suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) (err error) { // run before scenario handlers for _, f := range s.beforeScenarioHandlers { f(scenario) @@ -448,7 +417,7 @@ func (s *suite) runScenario(scenario *gherkin.Scenario, b *gherkin.Background) ( return } -func (s *suite) printStepDefinitions() { +func (s *Suite) printStepDefinitions() { var longest int for _, def := range s.steps { if longest < len(def.Expr.String()) { @@ -465,7 +434,7 @@ func (s *suite) printStepDefinitions() { } } -func (s *suite) parseFeatures() (err error) { +func (s *Suite) parseFeatures() (err error) { for _, pat := range s.paths { // check if line number is specified parts := strings.Split(pat, ":") @@ -525,7 +494,7 @@ func (s *suite) parseFeatures() (err error) { return } -func (s *suite) applyTagFilter(ft *gherkin.Feature) { +func (s *Suite) applyTagFilter(ft *gherkin.Feature) { if len(s.tags) == 0 { return } @@ -585,7 +554,7 @@ func hasTag(tags []string, tag string) bool { } // based on http://behat.readthedocs.org/en/v2.5/guides/6.cli.html#gherkin-filters -func (s *suite) matchesTags(tags []string) (ok bool) { +func (s *Suite) matchesTags(tags []string) (ok bool) { ok = true for _, andTags := range strings.Split(s.tags, "&&") { var okComma bool diff --git a/suite_test.go b/suite_test.go index 2bd5c31..f7c175d 100644 --- a/suite_test.go +++ b/suite_test.go @@ -8,7 +8,7 @@ import ( "github.com/cucumber/gherkin-go" ) -func SuiteContext(s Suite) { +func SuiteContext(s *Suite) { c := &suiteContext{} s.BeforeScenario(c.ResetBeforeEachScenario) @@ -50,7 +50,7 @@ type firedEvent struct { } type suiteContext struct { - testedSuite *suite + testedSuite *Suite events []*firedEvent fmt *testFormatter } @@ -58,7 +58,7 @@ type suiteContext struct { func (s *suiteContext) ResetBeforeEachScenario(interface{}) { // reset whole suite with the state s.fmt = &testFormatter{} - s.testedSuite = &suite{fmt: s.fmt} + s.testedSuite = &Suite{fmt: s.fmt} // our tested suite will have the same context registered SuiteContext(s.testedSuite) // reset all fired events diff --git a/tag_filter_test.go b/tag_filter_test.go index d49ae4f..817448b 100644 --- a/tag_filter_test.go +++ b/tag_filter_test.go @@ -5,14 +5,14 @@ import ( ) func assertNotMatchesTagFilter(tags []string, filter string, t *testing.T) { - s := &suite{tags: filter} + s := &Suite{tags: filter} if s.matchesTags(tags) { t.Errorf(`expected tags: %v not to match tag filter "%s", but it did`, tags, filter) } } func assertMatchesTagFilter(tags []string, filter string, t *testing.T) { - s := &suite{tags: filter} + s := &Suite{tags: filter} if !s.matchesTags(tags) { t.Errorf(`expected tags: %v to match tag filter "%s", but it did not`, tags, filter) }