instead of Suite interface expose Suite struct

* 570d70a update examples regarding Suite interface removal, closes #11
Этот коммит содержится в:
gedi 2015-07-03 11:39:46 +03:00
родитель 46f5218d36
коммит ca36316b7a
19 изменённых файлов: 142 добавлений и 234 удалений

Просмотреть файл

@ -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 youve 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

Просмотреть файл

@ -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,27 +122,30 @@ 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.SelectorExpr:
switch t := x.X.(type) {
case *ast.Ident:
if expr.Name == "Suite" {
if t.Name == "godog" && x.Sel.Name == "Suite" {
b.Contexts = append(b.Contexts, fun.Name.Name)
}
}
}
}
}
}
}
}
func (b *builder) merge() (*ast.File, error) {

Просмотреть файл

@ -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)

Просмотреть файл

@ -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)

Двоичные данные
examples/api/screenshots/undefined.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 151 КиБ

После

Ширина:  |  Высота:  |  Размер: 148 КиБ

Просмотреть файл

@ -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)

9
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

5
examples/godogs/godog.go Обычный файл
Просмотреть файл

@ -0,0 +1,5 @@
package main
var Godogs int
func main() { /* usual main func */ }

37
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
})
}

Просмотреть файл

@ -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

Просмотреть файл

@ -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
"""

Просмотреть файл

@ -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
})
}

Просмотреть файл

@ -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)
}

Просмотреть файл

@ -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", "")

2
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}}
}
`))

Двоичные данные
screenshots/undefined.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 96 КиБ

После

Ширина:  |  Высота:  |  Размер: 109 КиБ

109
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

Просмотреть файл

@ -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

Просмотреть файл

@ -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)
}