diff --git a/README.md b/README.md index 2f272bc..94d2ae3 100644 --- a/README.md +++ b/README.md @@ -46,24 +46,25 @@ You should see that the steps are undefined: ![Screenshot](https://raw.github.com/DATA-DOG/godog/master/screenshots/undefined.png) -**NOTE:** the undefined step templates are still in development and scheduled for **0.2.0** +It gives you undefined step snippets to implement in your test context. You may copy these snippets +into **godog_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. ``` go /* file: /tmp/godog/godog.go */ package main -type GodogCart struct { - reserve int -} - -func (c *GodogCart) Eat(num int) { c.reserve -= num } - -func (c *GodogCart) Available() int { return c.reserve } +var Godogs int func main() { /* usual main func */ } ``` -Now lets describe all steps to test the application behavior: +Now lets finish our step implementations in order to test our feature requirements: ``` go /* file: /tmp/godog/godog_test.go */ @@ -75,35 +76,34 @@ import ( "github.com/DATA-DOG/godog" ) -func (c *GodogCart) resetReserve(interface{}) { - c.reserve = 0 -} - -func (c *GodogCart) thereAreNumGodogsInReserve(avail int) error { - c.reserve = avail +func thereAreGodogs(available int) error { + Godogs = available return nil } -func (c *GodogCart) iEatNum(num int) error { - c.Eat(num) +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 (c *GodogCart) thereShouldBeNumRemaining(left int) error { - if c.Available() != left { - return fmt.Errorf("expected %d godogs to be remaining, but there is %d", left, c.Available()) +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 godogCartContext(s godog.Suite) { - c := &GodogCart{} - // each time before running scenario reset reserve - s.BeforeScenario(c.resetReserve) - // register steps - s.Step(`^there are (\d+) godogs?$`, c.thereAreNumGodogsInReserve) - s.Step(`^I eat (\d+)$`, c.iEatNum) - s.Step(`^there should be (\d+) remaining$`, c.thereShouldBeNumRemaining) +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 + }) } ``` @@ -111,6 +111,10 @@ Now when you run the `godog godog.feature` again, you should see: ![Screenshot](https://raw.github.com/DATA-DOG/godog/master/screenshots/passed.png) +**Note:** we have hooked to **BeforeScenario** event in order to reset state. You may hook into +more events, like **AfterStep** to test against an error and print more details about the error +or state before failure. Or **BeforeSuite** to prepare a database. + ### Documentation See [godoc][godoc] for general API details. diff --git a/fmt.go b/fmt.go index e34fc19..92ab32f 100644 --- a/fmt.go +++ b/fmt.go @@ -1,13 +1,45 @@ package godog import ( + "bytes" "fmt" + "regexp" "strings" + "text/template" "time" + "unicode" "github.com/cucumber/gherkin-go" ) +// some snippet formatting regexps +var snippetExprCleanup = regexp.MustCompile("([\\/\\[\\]\\(\\)\\\\^\\$\\.\\|\\?\\*\\+\\'])") +var snippetExprQuoted = regexp.MustCompile("(\\s*|^)\"(?:[^\"]*)\"(\\s+|$)") +var snippetMethodName = regexp.MustCompile("[^a-zA-Z\\_\\ ]") +var snippetNumbers = regexp.MustCompile("(\\d+)") + +var snippetHelperFuncs = template.FuncMap{ + "backticked": func(s string) string { + return "`" + s + "`" + }, +} + +var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(` +{{ range . }}func {{ .Method }}({{ .Args }}) error { + return godog.ErrPending +} + +{{end}}func featureContext(s godog.Suite) { {{ range . }} + s.Step({{ backticked .Expr }}, {{ .Method }}){{end}} +} +`)) + +type undefinedSnippet struct { + Method string + Expr string + argument interface{} // gherkin step argument +} + type registeredFormatter struct { name string fmt Formatter @@ -180,6 +212,8 @@ func (f *basefmt) Summary() { steps = append(steps, parts[len(parts)-1]) } if len(f.pending) > 0 { + passed -= len(f.pending) + parts = append(parts, cl(fmt.Sprintf("%d pending", len(f.pending)), yellow)) steps = append(steps, cl(fmt.Sprintf("%d pending", len(f.pending)), yellow)) } if len(f.undefined) > 0 { @@ -209,4 +243,100 @@ func (f *basefmt) Summary() { fmt.Println(fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", "))) } fmt.Println(elapsed) + + f.snippets() +} + +func (s *undefinedSnippet) Args() string { + var args []string + var pos, idx int + var breakLoop bool + for !breakLoop { + part := s.Expr[pos:] + ipos := strings.Index(part, "(\\d+)") + spos := strings.Index(part, "\"([^\"]*)\"") + switch { + case spos == -1 && ipos == -1: + breakLoop = true + case spos == -1: + idx++ + pos += ipos + len("(\\d+)") + args = append(args, fmt.Sprintf("arg%d int", idx)) + case ipos == -1: + idx++ + pos += spos + len("\"([^\"]*)\"") + args = append(args, fmt.Sprintf("arg%d string", idx)) + case ipos < spos: + idx++ + pos += ipos + len("(\\d+)") + args = append(args, fmt.Sprintf("arg%d int", idx)) + case spos < ipos: + idx++ + pos += spos + len("\"([^\"]*)\"") + args = append(args, fmt.Sprintf("arg%d string", idx)) + } + } + if s.argument != nil { + idx++ + switch s.argument.(type) { + case *gherkin.DocString: + args = append(args, fmt.Sprintf("arg%d *gherkin.DocString", idx)) + case *gherkin.DataTable: + args = append(args, fmt.Sprintf("arg%d *gherkin.DataTable", idx)) + } + } + return strings.Join(args, ", ") +} + +func (f *basefmt) snippets() { + if len(f.undefined) == 0 { + return + } + + fmt.Println(cl("\nYou can implement step definitions for undefined steps with these snippets:", yellow)) + + var index int + var snips []*undefinedSnippet + // build snippets + for _, u := range f.undefined { + expr := snippetExprCleanup.ReplaceAllString(u.step.Text, "\\\\$1") + expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") + expr = snippetExprQuoted.ReplaceAllString(expr, " \"([^\"]*)\" ") + expr = "^" + strings.TrimSpace(expr) + "$" + + name := snippetNumbers.ReplaceAllString(u.step.Text, "") + name = snippetExprQuoted.ReplaceAllString(name, "") + name = snippetMethodName.ReplaceAllString(name, "") + var words []string + for i, w := range strings.Split(name, " ") { + if i != 0 { + w = strings.Title(w) + } else { + w = string(unicode.ToLower(rune(w[0]))) + w[1:] + } + words = append(words, w) + } + name = strings.Join(words, "") + if len(name) == 0 { + index++ + name = fmt.Sprintf("stepDefinition%d", index) + } + + var found bool + for _, snip := range snips { + if snip.Expr == expr { + found = true + break + } + } + if !found { + snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: u.step.Argument}) + } + } + + var buf bytes.Buffer + if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil { + panic(err) + } + fmt.Println(cl(buf.String(), yellow)) } diff --git a/screenshots/passed.png b/screenshots/passed.png index 8c8dba7..2897ac5 100644 Binary files a/screenshots/passed.png and b/screenshots/passed.png differ diff --git a/screenshots/undefined.png b/screenshots/undefined.png index 9a3fc8b..a26cfb2 100644 Binary files a/screenshots/undefined.png and b/screenshots/undefined.png differ