Added the new TestSuiteInitializer and ScenarioInitializer

Этот коммит содержится в:
Fredrik Lönnblad 2020-05-16 18:35:25 +02:00
родитель 7568b291e4
коммит e6227a2e0f
14 изменённых файлов: 470 добавлений и 115 удалений

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

@ -160,7 +160,6 @@ import (
"fmt"
"github.com/cucumber/godog"
messages "github.com/cucumber/messages-go/v10"
)
func thereAreGodogs(available int) error {
@ -183,12 +182,12 @@ func thereShouldBeRemaining(remaining int) error {
return nil
}
func FeatureContext(s *godog.Suite) {
func ScenarioContext(s *godog.ScenarioContext) {
s.Step(`^there are (\d+) godogs$`, thereAreGodogs)
s.Step(`^I eat (\d+)$`, iEat)
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
s.BeforeScenario(func(*messages.Pickle) {
s.BeforeScenario(func(*godog.Scenario) {
Godogs = 0 // clean the state before every scenario
})
}
@ -250,22 +249,24 @@ The following example binds **godog** flags with specified prefix `godog`
in order to prevent flag collisions.
``` go
var opt = godog.Options{
var opts = godog.Options{
Output: colors.Colored(os.Stdout),
Format: "progress", // can define default values
}
func init() {
godog.BindFlags("godog.", flag.CommandLine, &opt)
godog.BindFlags("godog.", flag.CommandLine, &opts)
}
func TestMain(m *testing.M) {
flag.Parse()
opt.Paths = flag.Args()
opts.Paths = flag.Args()
status := godog.RunWithOptions("godogs", func(s *godog.Suite) {
FeatureContext(s)
}, opt)
status := godog.TestSuite{
Name: "godogs",
ScenarioInitializer: ScenarioContext,
Options: &opts,
}.Run()
if st := m.Run(); st > status {
status = st
@ -287,13 +288,17 @@ configuring needed options.
``` go
func TestMain(m *testing.M) {
status := godog.RunWithOptions("godog", func(s *godog.Suite) {
FeatureContext(s)
}, godog.Options{
opts := godog.Options{
Format: "progress",
Paths: []string{"features"},
Randomize: time.Now().UTC().UnixNano(), // randomize scenario execution order
})
}
status := godog.TestSuite{
Name: "godogs",
ScenarioInitializer: ScenarioContext,
Options: opts,
}.Run()
if st := m.Run(); st > status {
status = st
@ -315,12 +320,17 @@ func TestMain(m *testing.M) {
break
}
}
status := godog.RunWithOptions("godog", func(s *godog.Suite) {
godog.SuiteContext(s)
}, godog.Options{
opts := godog.Options{
Format: format,
Paths: []string{"features"},
})
}
status := godog.TestSuite{
Name: "godogs",
ScenarioInitializer: ScenarioContext,
Options: opts,
}.Run()
if st := m.Run(); st > status {
status = st

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

@ -56,7 +56,6 @@ need to store state within steps (a response), we should introduce a structure w
package main
import (
"github.com/cucumber/gherkin-go/v11"
"github.com/cucumber/godog"
)
@ -71,11 +70,11 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
return godog.ErrPending
}
func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) error {
func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) error {
return godog.ErrPending
}
func FeatureContext(s *godog.Suite) {
func ScenarioContext(s *godog.ScenarioContext) {
api := &apiFeature{}
s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, api.iSendrequestTo)
s.Step(`^the response code should be (\d+)$`, api.theResponseCodeShouldBe)
@ -98,7 +97,6 @@ import (
"net/http"
"net/http/httptest"
"github.com/cucumber/gherkin-go/v11"
"github.com/cucumber/godog"
)
@ -142,7 +140,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
return nil
}
func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) error {
func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) error {
var expected, actual []byte
var data interface{}
if err = json.Unmarshal([]byte(body.Content), &data); err != nil {
@ -158,7 +156,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgumen
return
}
func FeatureContext(s *godog.Suite) {
func ScenarioContext(s *godog.ScenarioContext) {
api := &apiFeature{}
s.BeforeScenario(api.resetResponse)

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

@ -8,14 +8,13 @@ import (
"reflect"
"github.com/cucumber/godog"
"github.com/cucumber/messages-go/v10"
)
type apiFeature struct {
resp *httptest.ResponseRecorder
}
func (a *apiFeature) resetResponse(*messages.Pickle) {
func (a *apiFeature) resetResponse(*godog.Scenario) {
a.resp = httptest.NewRecorder()
}
@ -51,7 +50,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
return nil
}
func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) (err error) {
func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) (err error) {
var expected, actual interface{}
// re-encode expected response
@ -71,7 +70,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgumen
return nil
}
func FeatureContext(s *godog.Suite) {
func ScenarioContext(s *godog.ScenarioContext) {
api := &apiFeature{}
s.BeforeScenario(api.resetResponse)

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

@ -6,10 +6,10 @@ Feature: eat godogs
Scenario: Eat 5 out of 12
Given there are 12 godogs
When I eat 4
When I eat 5
Then there should be 7 remaining
Scenario: Eat 12 out of 12
Given there are 12 godogs
When I eat 11
When I eat 12
Then there should be none remaining

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

@ -1,4 +1,3 @@
/* file: $GOPATH/src/assert-godogs/godogs_test.go */
package main
import (
@ -9,23 +8,24 @@ import (
"github.com/cucumber/godog"
"github.com/cucumber/godog/colors"
messages "github.com/cucumber/messages-go/v10"
"github.com/stretchr/testify/assert"
)
var opt = godog.Options{Output: colors.Colored(os.Stdout)}
var opts = godog.Options{Output: colors.Colored(os.Stdout)}
func init() {
godog.BindFlags("godog.", flag.CommandLine, &opt)
godog.BindFlags("godog.", flag.CommandLine, &opts)
}
func TestMain(m *testing.M) {
flag.Parse()
opt.Paths = flag.Args()
opts.Paths = flag.Args()
status := godog.RunWithOptions("godogs", func(s *godog.Suite) {
FeatureContext(s)
}, opt)
status := godog.TestSuite{
Name: "godogs",
ScenarioInitializer: ScenarioContext,
Options: &opts,
}.Run()
if st := m.Run(); st > status {
status = st
@ -65,13 +65,13 @@ func thereShouldBeNoneRemaining() error {
)
}
func FeatureContext(s *godog.Suite) {
func ScenarioContext(s *godog.ScenarioContext) {
s.Step(`^there are (\d+) godogs$`, thereAreGodogs)
s.Step(`^I eat (\d+)$`, iEat)
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
s.Step(`^there should be none remaining$`, thereShouldBeNoneRemaining)
s.BeforeScenario(func(*messages.Pickle) {
s.BeforeScenario(func(*godog.Scenario) {
Godogs = 0 // clean the state before every scenario
})
}

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

@ -11,7 +11,6 @@ import (
txdb "github.com/DATA-DOG/go-txdb"
"github.com/cucumber/godog"
"github.com/cucumber/messages-go/v10"
)
func init() {
@ -24,7 +23,7 @@ type apiFeature struct {
resp *httptest.ResponseRecorder
}
func (a *apiFeature) resetResponse(*messages.Pickle) {
func (a *apiFeature) resetResponse(*godog.Scenario) {
a.resp = httptest.NewRecorder()
if a.db != nil {
a.db.Close()
@ -71,7 +70,7 @@ func (a *apiFeature) theResponseCodeShouldBe(code int) error {
return nil
}
func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgument_PickleDocString) (err error) {
func (a *apiFeature) theResponseShouldMatchJSON(body *godog.DocString) (err error) {
var expected, actual interface{}
// re-encode expected response
@ -91,7 +90,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *messages.PickleStepArgumen
return nil
}
func (a *apiFeature) thereAreUsers(users *messages.PickleStepArgument_PickleTable) error {
func (a *apiFeature) thereAreUsers(users *godog.Table) error {
var fields []string
var marks []string
head := users.Rows[0].Cells
@ -123,7 +122,7 @@ func (a *apiFeature) thereAreUsers(users *messages.PickleStepArgument_PickleTabl
return nil
}
func FeatureContext(s *godog.Suite) {
func ScenarioContext(s *godog.ScenarioContext) {
api := &apiFeature{}
s.BeforeScenario(api.resetResponse)

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

@ -1,4 +1,3 @@
/* file: $GOPATH/src/godogs/godogs_test.go */
package main
import (
@ -9,22 +8,23 @@ import (
"github.com/cucumber/godog"
"github.com/cucumber/godog/colors"
messages "github.com/cucumber/messages-go/v10"
)
var opt = godog.Options{Output: colors.Colored(os.Stdout)}
var opts = godog.Options{Output: colors.Colored(os.Stdout)}
func init() {
godog.BindFlags("godog.", flag.CommandLine, &opt)
godog.BindFlags("godog.", flag.CommandLine, &opts)
}
func TestMain(m *testing.M) {
flag.Parse()
opt.Paths = flag.Args()
opts.Paths = flag.Args()
status := godog.RunWithOptions("godogs", func(s *godog.Suite) {
FeatureContext(s)
}, opt)
status := godog.TestSuite{
Name: "godogs",
ScenarioInitializer: ScenarioContext,
Options: &opts,
}.Run()
if st := m.Run(); st > status {
status = st
@ -52,12 +52,12 @@ func thereShouldBeRemaining(remaining int) error {
return nil
}
func FeatureContext(s *godog.Suite) {
func ScenarioContext(s *godog.ScenarioContext) {
s.Step(`^there are (\d+) godogs$`, thereAreGodogs)
s.Step(`^I eat (\d+)$`, iEat)
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
s.BeforeScenario(func(*messages.Pickle) {
s.BeforeScenario(func(*godog.Scenario) {
Godogs = 0 // clean the state before every scenario
})
}

6
ast.go
Просмотреть файл

@ -2,7 +2,7 @@ package godog
import "go/ast"
func astContexts(f *ast.File) []string {
func astContexts(f *ast.File, selectName string) []string {
var contexts []string
for _, d := range f.Decls {
switch fun := d.(type) {
@ -12,13 +12,13 @@ func astContexts(f *ast.File) []string {
case *ast.StarExpr:
switch x := expr.X.(type) {
case *ast.Ident:
if x.Name == "Suite" {
if x.Name == selectName {
contexts = append(contexts, fun.Name.Name)
}
case *ast.SelectorExpr:
switch t := x.X.(type) {
case *ast.Ident:
if t.Name == "godog" && x.Sel.Name == "Suite" {
if t.Name == "godog" && x.Sel.Name == selectName {
contexts = append(contexts, fun.Name.Name)
}
}

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

@ -34,7 +34,7 @@ func astContextParse(src string, t *testing.T) []string {
t.Fatalf("unexpected error while parsing ast: %v", err)
}
return astContexts(f)
return astContexts(f, "Suite")
}
func TestShouldGetSingleContextFromSource(t *testing.T) {

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

@ -30,28 +30,51 @@ var (
import (
"github.com/cucumber/godog"
{{if .Contexts}}_test "{{.ImportPath}}"{{end}}
{{if .XContexts}}_xtest "{{.ImportPath}}_test"{{end}}
{{if .XContexts}}"testing/internal/testdeps"{{end}}
{{if or .DeprecatedFeatureContexts .TestSuiteContexts .ScenarioContexts}}_test "{{.ImportPath}}"{{end}}
{{if or .XDeprecatedFeatureContexts .XTestSuiteContexts .XScenarioContexts}}_xtest "{{.ImportPath}}_test"{{end}}
{{if or .XDeprecatedFeatureContexts .XTestSuiteContexts .XScenarioContexts}}"testing/internal/testdeps"{{end}}
"os"
)
{{if .XContexts}}
{{if or .XDeprecatedFeatureContexts .XTestSuiteContexts .XScenarioContexts}}
func init() {
testdeps.ImportPath = "{{.ImportPath}}"
}
{{end}}
func main() {
{{if or .TestSuiteContexts .ScenarioContexts .XTestSuiteContexts .XScenarioContexts}}
status := godog.TestSuite{
Name: "{{ .Name }}",
TestSuiteInitializer: func (ctx *godog.TestSuiteContext) {
os.Setenv("GODOG_TESTED_PACKAGE", "{{.ImportPath}}")
{{range .TestSuiteContexts}}
_test.{{ . }}(ctx)
{{end}}
{{range .XTestSuiteContexts}}
_xtest.{{ . }}(ctx)
{{end}}
},
ScenarioInitializer: func (ctx *godog.ScenarioContext) {
{{range .ScenarioContexts}}
_test.{{ . }}(ctx)
{{end}}
{{range .XScenarioContexts}}
_xtest.{{ . }}(ctx)
{{end}}
},
}.Run()
{{else}}
status := godog.Run("{{ .Name }}", func (suite *godog.Suite) {
os.Setenv("GODOG_TESTED_PACKAGE", "{{.ImportPath}}")
{{range .Contexts}}
{{range .DeprecatedFeatureContexts}}
_test.{{ . }}(suite)
{{end}}
{{range .XContexts}}
{{range .XDeprecatedFeatureContexts}}
_xtest.{{ . }}(suite)
{{end}}
})
{{end}}
os.Exit(status)
}`))
@ -308,35 +331,44 @@ func buildTempFile(pkg *build.Package) ([]byte, error) {
// and produces a testmain source code.
func buildTestMain(pkg *build.Package) ([]byte, error) {
var (
contexts []string
xcontexts []string
err error
name, importPath string
ctxs, xctxs contexts
err error
name = "main"
importPath string
)
if nil != pkg {
contexts, err = processPackageTestFiles(pkg.TestGoFiles)
if err != nil {
if ctxs, err = processPackageTestFiles(pkg.TestGoFiles); err != nil {
return nil, err
}
xcontexts, err = processPackageTestFiles(pkg.XTestGoFiles)
if err != nil {
if xctxs, err = processPackageTestFiles(pkg.XTestGoFiles); err != nil {
return nil, err
}
importPath = parseImport(pkg.ImportPath, pkg.Root)
name = pkg.Name
} else {
name = "main"
}
data := struct {
Name string
Contexts []string
XContexts []string
ImportPath string
Name string
ImportPath string
DeprecatedFeatureContexts []string
TestSuiteContexts []string
ScenarioContexts []string
XDeprecatedFeatureContexts []string
XTestSuiteContexts []string
XScenarioContexts []string
}{
Name: name,
Contexts: contexts,
XContexts: xcontexts,
ImportPath: importPath,
Name: name,
ImportPath: importPath,
DeprecatedFeatureContexts: ctxs.deprecatedFeatureCtxs,
TestSuiteContexts: ctxs.testSuiteCtxs,
ScenarioContexts: ctxs.scenarioCtxs,
XDeprecatedFeatureContexts: xctxs.deprecatedFeatureCtxs,
XTestSuiteContexts: xctxs.testSuiteCtxs,
XScenarioContexts: xctxs.scenarioCtxs,
}
var buf bytes.Buffer
@ -380,11 +412,38 @@ func parseImport(rawPath, rootPath string) string {
return mod.Path + filepath.ToSlash(strings.TrimPrefix(rawPath, normaliseLocalImportPath(mod.Dir)))
}
type contexts struct {
deprecatedFeatureCtxs []string
testSuiteCtxs []string
scenarioCtxs []string
}
func (ctxs contexts) validate() error {
var allCtxs []string
allCtxs = append(allCtxs, ctxs.deprecatedFeatureCtxs...)
allCtxs = append(allCtxs, ctxs.testSuiteCtxs...)
allCtxs = append(allCtxs, ctxs.scenarioCtxs...)
var failed []string
for _, ctx := range allCtxs {
runes := []rune(ctx)
if unicode.IsLower(runes[0]) {
expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...)
failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected)))
}
}
if len(failed) > 0 {
return fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t"))
}
return nil
}
// processPackageTestFiles runs through ast of each test
// file pack and looks for godog suite contexts to register
// on run
func processPackageTestFiles(packs ...[]string) ([]string, error) {
var ctxs []string
func processPackageTestFiles(packs ...[]string) (ctxs contexts, _ error) {
fset := token.NewFileSet()
for _, pack := range packs {
for _, testFile := range pack {
@ -393,21 +452,13 @@ func processPackageTestFiles(packs ...[]string) ([]string, error) {
return ctxs, err
}
ctxs = append(ctxs, astContexts(node)...)
ctxs.deprecatedFeatureCtxs = append(ctxs.deprecatedFeatureCtxs, astContexts(node, "Suite")...)
ctxs.testSuiteCtxs = append(ctxs.testSuiteCtxs, astContexts(node, "TestSuiteContext")...)
ctxs.scenarioCtxs = append(ctxs.scenarioCtxs, astContexts(node, "ScenarioContext")...)
}
}
var failed []string
for _, ctx := range ctxs {
runes := []rune(ctx)
if unicode.IsLower(runes[0]) {
expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...)
failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected)))
}
}
if len(failed) > 0 {
return ctxs, fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t"))
}
return ctxs, nil
return ctxs, ctxs.validate()
}
func findToolDir() string {

125
run.go
Просмотреть файл

@ -20,6 +20,8 @@ const (
)
type initializer func(*Suite)
type testSuiteInitializer func(*TestSuiteContext)
type scenarioInitializer func(*ScenarioContext)
type runner struct {
randomSeed int64
@ -27,6 +29,8 @@ type runner struct {
features []*feature
fmt Formatter
initializer initializer
testSuiteInitializer testSuiteInitializer
scenarioInitializer scenarioInitializer
}
func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool) {
@ -38,8 +42,17 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool
useFmtCopy = true
}
testSuiteContext := TestSuiteContext{}
if r.testSuiteInitializer != nil {
r.testSuiteInitializer(&testSuiteContext)
}
r.fmt.TestRunStarted()
// run before suite handlers
for _, f := range testSuiteContext.beforeSuiteHandlers {
f()
}
queue := make(chan int, rate)
for i, ft := range r.features {
queue <- i // reserve space in queue
@ -74,7 +87,14 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool
suite.fmt = r.fmt
}
r.initializer(suite)
if r.initializer != nil {
r.initializer(suite)
}
if r.scenarioInitializer != nil {
suite.scenarioInitializer = r.scenarioInitializer
}
suite.run()
if suite.failed {
copyLock.Lock()
@ -105,6 +125,11 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool
}
close(queue)
// run after suite handlers
for _, f := range testSuiteContext.afterSuiteHandlers {
f()
}
// print summary
r.fmt.Summary()
return
@ -126,7 +151,22 @@ func (r *runner) concurrent(rate int, formatterFn func() Formatter) (failed bool
//
// If there are flag related errors they will
// be directed to os.Stderr
func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Options) int {
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios.
// Use:
// godog.TestSuite{
// Name: name,
// TestSuiteInitializer: testSuiteInitializer,
// ScenarioInitializer: scenarioInitializer,
// Options: &opts,
// }.Run()
// instead.
func RunWithOptions(suite string, initializer func(*Suite), opt Options) int {
return runWithOptions(suite, runner{initializer: initializer}, opt)
}
func runWithOptions(suite string, runner runner, opt Options) int {
var output io.Writer = os.Stdout
if nil != opt.Output {
output = opt.Output
@ -140,7 +180,7 @@ func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Opt
if opt.ShowStepDefinitions {
s := &Suite{}
contextInitializer(s)
runner.initializer(s)
s.printStepDefinitions(output)
return exitOptionError
}
@ -169,35 +209,30 @@ func RunWithOptions(suite string, contextInitializer func(suite *Suite), opt Opt
))
return exitOptionError
}
runner.fmt = formatter(suite, output)
features, err := parseFeatures(opt.Tags, opt.Paths)
if err != nil {
var err error
if runner.features, err = parseFeatures(opt.Tags, opt.Paths); err != nil {
fmt.Fprintln(os.Stderr, err)
return exitOptionError
}
// user may have specified -1 option to create random seed
randomize := opt.Randomize
if randomize == -1 {
randomize = makeRandomSeed()
runner.randomSeed = opt.Randomize
if runner.randomSeed == -1 {
runner.randomSeed = makeRandomSeed()
}
r := runner{
fmt: formatter(suite, output),
initializer: contextInitializer,
features: features,
randomSeed: randomize,
stopOnFailure: opt.StopOnFailure,
strict: opt.Strict,
}
runner.stopOnFailure = opt.StopOnFailure
runner.strict = opt.Strict
// store chosen seed in environment, so it could be seen in formatter summary report
os.Setenv("GODOG_SEED", strconv.FormatInt(r.randomSeed, 10))
os.Setenv("GODOG_SEED", strconv.FormatInt(runner.randomSeed, 10))
// determine tested package
_, filename, _, _ := runtime.Caller(1)
os.Setenv("GODOG_TESTED_PACKAGE", runsFromPackage(filename))
failed := r.concurrent(opt.Concurrency, func() Formatter { return formatter(suite, output) })
failed := runner.concurrent(opt.Concurrency, func() Formatter { return formatter(suite, output) })
// @TODO: should prevent from having these
os.Setenv("GODOG_SEED", "")
@ -240,9 +275,20 @@ func runsFromPackage(fp string) string {
//
// If there are flag related errors they will
// be directed to os.Stderr
func Run(suite string, contextInitializer func(suite *Suite)) int {
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios.
// Use:
// godog.TestSuite{
// Name: name,
// TestSuiteInitializer: testSuiteInitializer,
// ScenarioInitializer: scenarioInitializer,
// }.Run()
// instead.
func Run(suite string, initializer func(*Suite)) int {
var opt Options
opt.Output = colors.Colored(os.Stdout)
flagSet := FlagSet(&opt)
if err := flagSet.Parse(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
@ -251,5 +297,44 @@ func Run(suite string, contextInitializer func(suite *Suite)) int {
opt.Paths = flagSet.Args()
return RunWithOptions(suite, contextInitializer, opt)
return RunWithOptions(suite, initializer, opt)
}
// TestSuite allows for configuration
// of the Test Suite Execution
type TestSuite struct {
Name string
TestSuiteInitializer func(*TestSuiteContext)
ScenarioInitializer func(*ScenarioContext)
Options *Options
}
// Run will execute the test suite.
//
// If options are not set, it will reads
// all configuration options from flags.
//
// The exit codes may vary from:
// 0 - success
// 1 - failed
// 2 - command line usage error
// 128 - or higher, os signal related error exit codes
//
// If there are flag related errors they will be directed to os.Stderr
func (ts TestSuite) Run() int {
if ts.Options == nil {
ts.Options = &Options{}
ts.Options.Output = colors.Colored(os.Stdout)
flagSet := FlagSet(ts.Options)
if err := flagSet.Parse(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
return exitOptionError
}
ts.Options.Paths = flagSet.Args()
}
r := runner{testSuiteInitializer: ts.TestSuiteInitializer, scenarioInitializer: ts.ScenarioInitializer}
return runWithOptions(ts.Name, r, *ts.Options)
}

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

@ -165,6 +165,10 @@ var ErrPending = fmt.Errorf("step implementation is pending")
// in order to have a trace information. Only step
// executions are catching panic error since it may
// be a context specific error.
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios.
// This struct will therefore not be exported in the future.
type Suite struct {
steps []*StepDefinition
features []*feature
@ -175,6 +179,8 @@ type Suite struct {
stopOnFailure bool
strict bool
scenarioInitializer scenarioInitializer
// suite event handlers
beforeSuiteHandlers []func()
beforeFeatureHandlers []func(*messages.GherkinDocument)
@ -201,6 +207,10 @@ type Suite struct {
// If none of the *StepDefinition is matched, then
// ErrUndefined error will be returned when
// running steps.
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios. Use
// func (ctx *ScenarioContext) Step instead.
func (s *Suite) Step(expr interface{}, stepFunc interface{}) {
var regex *regexp.Regexp
@ -254,6 +264,10 @@ func (s *Suite) Step(expr interface{}, stepFunc interface{}) {
//
// Use it to prepare the test suite for a spin.
// Connect and prepare database for instance...
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios. Use
// func (ctx *TestSuiteContext) BeforeSuite instead.
func (s *Suite) BeforeSuite(fn func()) {
s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, fn)
}
@ -285,12 +299,20 @@ func (s *Suite) BeforeFeature(fn func(*messages.GherkinDocument)) {
// It is a good practice to restore the default state
// before every scenario so it would be isolated from
// any kind of state.
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios. Use
// func (ctx *ScenarioContext) BeforeScenario instead.
func (s *Suite) BeforeScenario(fn func(*messages.Pickle)) {
s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, fn)
}
// BeforeStep registers a function or method
// to be run before every step.
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios. Use
// func (ctx *ScenarioContext) BeforeStep instead.
func (s *Suite) BeforeStep(fn func(*messages.Pickle_PickleStep)) {
s.beforeStepHandlers = append(s.beforeStepHandlers, fn)
}
@ -304,12 +326,20 @@ func (s *Suite) BeforeStep(fn func(*messages.Pickle_PickleStep)) {
//
// In some cases, for example when running a headless
// browser, to take a screenshot after failure.
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios. Use
// func (ctx *ScenarioContext) AfterStep instead.
func (s *Suite) AfterStep(fn func(*messages.Pickle_PickleStep, error)) {
s.afterStepHandlers = append(s.afterStepHandlers, fn)
}
// AfterScenario registers an function or method
// to be run after every pickle.
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios. Use
// func (ctx *ScenarioContext) AfterScenario instead.
func (s *Suite) AfterScenario(fn func(*messages.Pickle, error)) {
s.afterScenarioHandlers = append(s.afterScenarioHandlers, fn)
}
@ -326,6 +356,10 @@ func (s *Suite) AfterFeature(fn func(*messages.GherkinDocument)) {
// AfterSuite registers a function or method
// to be run once after suite runner
//
// Deprecated: The current Suite initializer will be removed and replaced by
// two initializers, one for the Test Suite and one for the Scenarios. Use
// func (ctx *TestSuiteContext) AfterSuite instead.
func (s *Suite) AfterSuite(fn func()) {
s.afterSuiteHandlers = append(s.afterSuiteHandlers, fn)
}
@ -552,6 +586,11 @@ func (s *Suite) runFeature(f *feature) {
}()
for _, pickle := range f.pickles {
if s.scenarioInitializer != nil {
sc := ScenarioContext{suite: s}
s.scenarioInitializer(&sc)
}
err := s.runPickle(pickle)
if s.shouldFail(err) {
s.failed = true

125
test_context.go Обычный файл
Просмотреть файл

@ -0,0 +1,125 @@
package godog
import "github.com/cucumber/messages-go/v10"
// Scenario represents the executed scenario
type Scenario = messages.Pickle
// Step represents the executed step
type Step = messages.Pickle_PickleStep
// DocString represents the DocString argument made to a step definition
type DocString = messages.PickleStepArgument_PickleDocString
// Table represents the Table argument made to a step definition
type Table = messages.PickleStepArgument_PickleTable
// TestSuiteContext allows various contexts
// to register event handlers.
//
// When running a test suite, the instance of TestSuiteContext
// 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
type TestSuiteContext struct {
beforeSuiteHandlers []func()
afterSuiteHandlers []func()
}
// BeforeSuite registers a function or method
// to be run once before suite runner.
//
// Use it to prepare the test suite for a spin.
// Connect and prepare database for instance...
func (ctx *TestSuiteContext) BeforeSuite(fn func()) {
ctx.beforeSuiteHandlers = append(ctx.beforeSuiteHandlers, fn)
}
// AfterSuite registers a function or method
// to be run once after suite runner
func (ctx *TestSuiteContext) AfterSuite(fn func()) {
ctx.afterSuiteHandlers = append(ctx.afterSuiteHandlers, fn)
}
// ScenarioContext allows various contexts
// to register steps and event handlers.
//
// When running a scenario, the instance of ScenarioContext
// 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 ScenarioContext struct {
suite *Suite
}
// BeforeScenario registers a function or method
// to be run before every scenario.
//
// It is a good practice to restore the default state
// before every scenario so it would be isolated from
// any kind of state.
func (ctx *ScenarioContext) BeforeScenario(fn func(sc *Scenario)) {
ctx.suite.BeforeScenario(fn)
}
// AfterScenario registers an function or method
// to be run after every scenario.
func (ctx *ScenarioContext) AfterScenario(fn func(sc *Scenario, err error)) {
ctx.suite.AfterScenario(fn)
}
// BeforeStep registers a function or method
// to be run before every step.
func (ctx *ScenarioContext) BeforeStep(fn func(st *Step)) {
ctx.suite.BeforeStep(fn)
}
// AfterStep registers an function or method
// to be run after every step.
//
// It may be convenient to return a different kind of error
// in order to print more state details which may help
// in case of step failure
//
// In some cases, for example when running a headless
// browser, to take a screenshot after failure.
func (ctx *ScenarioContext) AfterStep(fn func(st *Step, err error)) {
ctx.suite.AfterStep(fn)
}
// Step allows to register a *StepDefinition in the
// 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 or stepFunc is not a valid step
// handler.
//
// The expression can be of type: *regexp.Regexp, string or []byte
//
// The stepFunc may accept one or several arguments of type:
// - int, int8, int16, int32, int64
// - float32, float64
// - string
// - []byte
// - *godog.DocString
// - *godog.Table
//
// The stepFunc need to return either an error or []string for multistep
//
// 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 *StepDefinition is matched, then
// ErrUndefined error will be returned when
// running steps.
func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) {
ctx.suite.Step(expr, stepFunc)
}

49
test_context_test.go Обычный файл
Просмотреть файл

@ -0,0 +1,49 @@
package godog
import (
"bytes"
"strings"
"testing"
"github.com/cucumber/gherkin-go/v11"
"github.com/cucumber/godog/colors"
"github.com/cucumber/messages-go/v10"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_TestContext(t *testing.T) {
const path = "any.feature"
var buf bytes.Buffer
w := colors.Uncolored(&buf)
gd, err := gherkin.ParseGherkinDocument(strings.NewReader(basicGherkinFeature), (&messages.Incrementing{}).NewId)
require.NoError(t, err)
pickles := gherkin.Pickles(*gd, path, (&messages.Incrementing{}).NewId)
r := runner{
fmt: progressFunc("progress", w),
features: []*feature{{GherkinDocument: gd, pickles: pickles}},
testSuiteInitializer: nil,
scenarioInitializer: func(sc *ScenarioContext) {
sc.Step(`^one$`, func() error { return nil })
sc.Step(`^two$`, func() error { return nil })
},
}
failed := r.concurrent(1, func() Formatter { return progressFunc("progress", w) })
require.False(t, failed)
expected := `.. 2
1 scenarios (1 passed)
2 steps (2 passed)
0s
`
actual := buf.String()
assert.Equal(t, expected, actual)
}