From fbed999ad81a4aeffb265156d002490c5e98f9f1 Mon Sep 17 00:00:00 2001 From: tfreville Date: Thu, 10 Dec 2020 12:35:47 +0100 Subject: [PATCH 1/4] feat(step_definition): Allows to define step function without return. Issue: It is not possible to use function without return when matching steps, resulting in a lot of Nil only error returns. Fix: Allows to provide empty result function by correctly matching reflect Calls on step Handler. When nothing is returned by the Handler, it will return nil as if errors was nil. --- features/tags.feature | 1 + internal/models/stepdef.go | 6 +++++- internal/models/stepdef_test.go | 21 +++++++++++++++++++++ run_test.go | 3 ++- suite_context_test.go | 4 ++-- test_context.go | 30 ++++++++++++++++-------------- 6 files changed, 47 insertions(+), 18 deletions(-) diff --git a/features/tags.feature b/features/tags.feature index 06e3f20..bf182fd 100644 --- a/features/tags.feature +++ b/features/tags.feature @@ -10,6 +10,7 @@ Feature: tag filters Background: Given passing step + And passing step without return Scenario Outline: parse a scenario Given a feature path "" diff --git a/internal/models/stepdef.go b/internal/models/stepdef.go index 1b24013..d2a2419 100644 --- a/internal/models/stepdef.go +++ b/internal/models/stepdef.go @@ -156,7 +156,11 @@ func (sd *StepDefinition) Run() interface{} { } } - return sd.HandlerValue.Call(values)[0].Interface() + res := sd.HandlerValue.Call(values) + if len(res) == 0 { + return nil + } + return res[0].Interface() } func (sd *StepDefinition) shouldBeString(idx int) (string, error) { diff --git a/internal/models/stepdef_test.go b/internal/models/stepdef_test.go index 315a0fb..2e42f02 100644 --- a/internal/models/stepdef_test.go +++ b/internal/models/stepdef_test.go @@ -12,6 +12,27 @@ import ( "github.com/cucumber/godog/internal/models" ) +func TestShouldSupportEmptyHandlerReturn(t *testing.T) { + fn := func(a int64, b int32, c int16, d int8) {} + + def := &models.StepDefinition{ + StepDefinition: formatters.StepDefinition{ + Handler: fn, + }, + HandlerValue: reflect.ValueOf(fn), + } + + def.Args = []interface{}{"1", "1", "1", "1"} + if err := def.Run(); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + def.Args = []interface{}{"1", "1", "1", strings.Repeat("1", 9)} + if err := def.Run(); err == nil { + t.Fatalf("expected convertion fail for int8, but got none") + } +} + func TestShouldSupportIntTypes(t *testing.T) { fn := func(a int64, b int32, c int16, d int8) error { return nil } diff --git a/run_test.go b/run_test.go index b94018b..01fe4cf 100644 --- a/run_test.go +++ b/run_test.go @@ -568,7 +568,8 @@ type progressOutput struct { bottomRows []string } -func passingStepDef() error { return nil } +func passingStepDef() error { return nil } +func passingStepDefWithoutReturn() {} func oddEvenStepDef(odd, even int) error { return oddOrEven(odd, even) } diff --git a/suite_context_test.go b/suite_context_test.go index 12f20f0..8997b4f 100644 --- a/suite_context_test.go +++ b/suite_context_test.go @@ -108,6 +108,7 @@ func InitializeScenario(ctx *ScenarioContext) { return nil }) + ctx.Step(`^(?:a )?passing step without return$`, func() {}) ctx.BeforeStep(tc.inject) } @@ -424,9 +425,8 @@ func (tc *godogFeaturesScenario) aFeatureFile(path string, body *DocString) erro return err } -func (tc *godogFeaturesScenario) featurePath(path string) error { +func (tc *godogFeaturesScenario) featurePath(path string) { tc.paths = append(tc.paths, path) - return nil } func (tc *godogFeaturesScenario) parseFeatures() error { diff --git a/test_context.go b/test_context.go index c2eafa0..7ad539f 100644 --- a/test_context.go +++ b/test_context.go @@ -176,8 +176,8 @@ func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { panic(fmt.Sprintf("expected handler to be func, but got: %T", stepFunc)) } - if typ.NumOut() != 1 { - panic(fmt.Sprintf("expected handler to return only one value, but it has: %d", typ.NumOut())) + if typ.NumOut() > 1 { + panic(fmt.Sprintf("expected handler to return either zero or one value, but it has: %d", typ.NumOut())) } def := &models.StepDefinition{ @@ -188,19 +188,21 @@ func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { HandlerValue: v, } - typ = typ.Out(0) - switch typ.Kind() { - case reflect.Interface: - if !typ.Implements(errorInterface) { - panic(fmt.Sprintf("expected handler to return an error, but got: %s", typ.Kind())) + if typ.NumOut() == 1 { + typ = typ.Out(0) + switch typ.Kind() { + case reflect.Interface: + if !typ.Implements(errorInterface) { + panic(fmt.Sprintf("expected handler to return an error, but got: %s", typ.Kind())) + } + case reflect.Slice: + if typ.Elem().Kind() != reflect.String { + panic(fmt.Sprintf("expected handler to return []string for multistep, but got: []%s", typ.Kind())) + } + def.Nested = true + default: + panic(fmt.Sprintf("expected handler to return an error or []string, but got: %s", typ.Kind())) } - case reflect.Slice: - if typ.Elem().Kind() != reflect.String { - panic(fmt.Sprintf("expected handler to return []string for multistep, but got: []%s", typ.Kind())) - } - def.Nested = true - default: - panic(fmt.Sprintf("expected handler to return an error or []string, but got: %s", typ.Kind())) } ctx.suite.steps = append(ctx.suite.steps, def) From fca39e4e8960f36f0930852ab20faa14ca18bff7 Mon Sep 17 00:00:00 2001 From: tfreville Date: Thu, 10 Dec 2020 15:59:10 +0100 Subject: [PATCH 2/4] test(convey): Add goconvey for test to ease test writting. Add tests for panic in test_context initialization. Made as independ commit so it can be rollbacked. --- go.mod | 1 + go.sum | 4 +++ internal/models/stepdef.go | 40 ++++++++++++++--------- test_context.go | 5 ++- test_context_test.go | 67 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 test_context_test.go diff --git a/go.mod b/go.mod index 646ab05..791608a 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/cucumber/messages-go/v16 v16.0.1 github.com/hashicorp/go-memdb v1.3.0 github.com/hashicorp/go-uuid v1.0.2 // indirect + github.com/smartystreets/goconvey v1.6.4 github.com/spf13/cobra v1.1.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 diff --git a/go.sum b/go.sum index 6c851b3..beb9ba4 100644 --- a/go.sum +++ b/go.sum @@ -71,6 +71,7 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -109,6 +110,7 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -161,7 +163,9 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= diff --git a/internal/models/stepdef.go b/internal/models/stepdef.go index d2a2419..98b0a00 100644 --- a/internal/models/stepdef.go +++ b/internal/models/stepdef.go @@ -1,6 +1,7 @@ package models import ( + "errors" "fmt" "reflect" "strconv" @@ -12,6 +13,13 @@ import ( var typeOfBytes = reflect.TypeOf([]byte(nil)) +// matchable errors +var ( + ErrUnmatchedStepArgumentNumber = errors.New("func received more arguments than expected") + ErrCannotConvert = errors.New("cannot convert argument") + ErrUnsupportedArgumentType = errors.New("unsupported argument type") +) + // StepDefinition ... type StepDefinition struct { formatters.StepDefinition @@ -28,8 +36,9 @@ type StepDefinition struct { func (sd *StepDefinition) Run() interface{} { typ := sd.HandlerValue.Type() if len(sd.Args) < typ.NumIn() { - return fmt.Errorf("func expects %d arguments, which is more than %d matched from step", typ.NumIn(), len(sd.Args)) + return fmt.Errorf("%w: expected %d arguments, matched %d from step", ErrUnmatchedStepArgumentNumber, typ.NumIn(), len(sd.Args)) } + var values []reflect.Value for i := 0; i < typ.NumIn(); i++ { param := typ.In(i) @@ -41,7 +50,7 @@ func (sd *StepDefinition) Run() interface{} { } v, err := strconv.ParseInt(s, 10, 0) if err != nil { - return fmt.Errorf(`cannot convert argument %d: "%s" to int: %s`, i, s, err) + return fmt.Errorf(`%w %d: "%s" to int: %s`, ErrCannotConvert, i, s, err) } values = append(values, reflect.ValueOf(int(v))) case reflect.Int64: @@ -51,7 +60,7 @@ func (sd *StepDefinition) Run() interface{} { } v, err := strconv.ParseInt(s, 10, 64) if err != nil { - return fmt.Errorf(`cannot convert argument %d: "%s" to int64: %s`, i, s, err) + return fmt.Errorf(`%w %d: "%s" to int64: %s`, ErrCannotConvert, i, s, err) } values = append(values, reflect.ValueOf(int64(v))) case reflect.Int32: @@ -61,7 +70,7 @@ func (sd *StepDefinition) Run() interface{} { } v, err := strconv.ParseInt(s, 10, 32) if err != nil { - return fmt.Errorf(`cannot convert argument %d: "%s" to int32: %s`, i, s, err) + return fmt.Errorf(`%w %d: "%s" to int32: %s`, ErrCannotConvert, i, s, err) } values = append(values, reflect.ValueOf(int32(v))) case reflect.Int16: @@ -71,7 +80,7 @@ func (sd *StepDefinition) Run() interface{} { } v, err := strconv.ParseInt(s, 10, 16) if err != nil { - return fmt.Errorf(`cannot convert argument %d: "%s" to int16: %s`, i, s, err) + return fmt.Errorf(`%w %d: "%s" to int16: %s`, ErrCannotConvert, i, s, err) } values = append(values, reflect.ValueOf(int16(v))) case reflect.Int8: @@ -81,7 +90,7 @@ func (sd *StepDefinition) Run() interface{} { } v, err := strconv.ParseInt(s, 10, 8) if err != nil { - return fmt.Errorf(`cannot convert argument %d: "%s" to int8: %s`, i, s, err) + return fmt.Errorf(`%w %d: "%s" to int8: %s`, ErrCannotConvert, i, s, err) } values = append(values, reflect.ValueOf(int8(v))) case reflect.String: @@ -97,7 +106,7 @@ func (sd *StepDefinition) Run() interface{} { } v, err := strconv.ParseFloat(s, 64) if err != nil { - return fmt.Errorf(`cannot convert argument %d: "%s" to float64: %s`, i, s, err) + return fmt.Errorf(`%w %d: "%s" to float64: %s`, ErrCannotConvert, i, s, err) } values = append(values, reflect.ValueOf(v)) case reflect.Float32: @@ -107,7 +116,7 @@ func (sd *StepDefinition) Run() interface{} { } v, err := strconv.ParseFloat(s, 32) if err != nil { - return fmt.Errorf(`cannot convert argument %d: "%s" to float32: %s`, i, s, err) + return fmt.Errorf(`%w %d: "%s" to float32: %s`, ErrCannotConvert, i, s, err) } values = append(values, reflect.ValueOf(float32(v))) case reflect.Ptr: @@ -124,7 +133,7 @@ func (sd *StepDefinition) Run() interface{} { break } - return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *messages.PickleStepArgument_PickleDocString`, i, arg, arg) + return fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleDocString`, ErrCannotConvert, i, arg, arg) case "messages.PickleTable": if v, ok := arg.(*messages.PickleStepArgument); ok { values = append(values, reflect.ValueOf(v.DataTable)) @@ -136,9 +145,9 @@ func (sd *StepDefinition) Run() interface{} { break } - return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *messages.PickleStepArgument_PickleTable`, i, arg, arg) + return fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleStepArgument_PickleTable`, ErrCannotConvert, i, arg, arg) default: - return fmt.Errorf("the argument %d type %T is not supported %s", i, arg, param.Elem().String()) + return fmt.Errorf("%w: the argument %d type %T is not supported %s", ErrUnsupportedArgumentType, i, arg, param.Elem().String()) } case reflect.Slice: switch param { @@ -149,10 +158,10 @@ func (sd *StepDefinition) Run() interface{} { } values = append(values, reflect.ValueOf([]byte(s))) default: - return fmt.Errorf("the slice argument %d type %s is not supported", i, param.Kind()) + return fmt.Errorf("%w: the slice argument %d type %s is not supported", ErrUnsupportedArgumentType, i, param.Kind()) } default: - return fmt.Errorf("the argument %d type %s is not supported", i, param.Kind()) + return fmt.Errorf("%w: the argument %d type %s is not supported", ErrUnsupportedArgumentType, i, param.Kind()) } } @@ -160,6 +169,7 @@ func (sd *StepDefinition) Run() interface{} { if len(res) == 0 { return nil } + return res[0].Interface() } @@ -170,13 +180,13 @@ func (sd *StepDefinition) shouldBeString(idx int) (string, error) { return arg, nil case *messages.PickleStepArgument: if arg.DocString == nil { - return "", fmt.Errorf(`cannot convert DocString is not set`) + return "", fmt.Errorf(`%w %d: "%v" of type "%T": DocString is not set`, ErrCannotConvert, idx, arg, arg) } return arg.DocString.Content, nil case *messages.PickleDocString: return arg.Content, nil default: - return "", fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to string`, idx, arg, arg) + return "", fmt.Errorf(`%w %d: "%v" of type "%T" to string`, ErrCannotConvert, idx, arg, arg) } } diff --git a/test_context.go b/test_context.go index 7ad539f..5aef077 100644 --- a/test_context.go +++ b/test_context.go @@ -12,6 +12,9 @@ import ( "github.com/cucumber/godog/internal/models" ) +// matchable errors +var () + // Scenario represents the executed scenario type Scenario = messages.Pickle @@ -197,7 +200,7 @@ func (ctx *ScenarioContext) Step(expr, stepFunc interface{}) { } case reflect.Slice: if typ.Elem().Kind() != reflect.String { - panic(fmt.Sprintf("expected handler to return []string for multistep, but got: []%s", typ.Kind())) + panic(fmt.Sprintf("expected handler to return []string for multistep, but got: []%s", typ.Elem().Kind())) } def.Nested = true default: diff --git a/test_context_test.go b/test_context_test.go new file mode 100644 index 0000000..2f10a3b --- /dev/null +++ b/test_context_test.go @@ -0,0 +1,67 @@ +package godog + +import ( + "fmt" + "regexp" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestScenarioContext_Step(t *testing.T) { + Convey("When adding steps to ScenarioContext ", t, func() { + ctx := ScenarioContext{suite: &suite{}} + + Convey("It should accept steps defined with regexp.Regexp", func() { + re := regexp.MustCompile(`(?:it is a test)?.{10}x*`) + So(func() { ctx.Step(re, okEmptyResult) }, ShouldNotPanic) + }) + + Convey("It should accept steps defined with bytes slice", func() { + So(func() { ctx.Step([]byte("(?:it is a test)?.{10}x*"), okEmptyResult) }, ShouldNotPanic) + }) + + Convey("It should accept steps handler with empty return", func() { + So(func() { ctx.Step(".*", okEmptyResult) }, ShouldNotPanic) + }) + + Convey("It should accept steps handler with error return", func() { + So(func() { ctx.Step(".*", okErrorResult) }, ShouldNotPanic) + }) + + Convey("It should accept steps handler with string slice return", func() { + So(func() { ctx.Step(".*", okSliceResult) }, ShouldNotPanic) + }) + + Convey("It should panic if step expression is neither a string, regex or byte slice", func() { + So(func() { ctx.Step(1251, okSliceResult) }, ShouldPanicWith, fmt.Sprintf("expecting expr to be a *regexp.Regexp or a string, got type: %T", 12)) + }) + Convey("It should panic if step handler", func() { + Convey("is not a function", func() { + So(func() { ctx.Step(".*", 124) }, ShouldPanicWith, fmt.Sprintf("expected handler to be func, but got: %T", 12)) + }) + + Convey("has more than 1 return value", func() { + So(func() { ctx.Step(".*", nokLimitCase) }, ShouldPanicWith, fmt.Sprintf("expected handler to return either zero or one value, but it has: 2")) + So(func() { ctx.Step(".*", nokMore) }, ShouldPanicWith, fmt.Sprintf("expected handler to return either zero or one value, but it has: 5")) + }) + + Convey("return type is not an error or string slice or void", func() { + So(func() { ctx.Step(".*", nokInvalidReturnInterfaceType) }, ShouldPanicWith, "expected handler to return an error, but got: interface") + So(func() { ctx.Step(".*", nokInvalidReturnSliceType) }, ShouldPanicWith, "expected handler to return []string for multistep, but got: []int") + So(func() { ctx.Step(".*", nokInvalidReturnOtherType) }, ShouldPanicWith, "expected handler to return an error or []string, but got: chan") + }) + }) + + }) + +} + +func okEmptyResult() {} +func okErrorResult() error { return nil } +func okSliceResult() []string { return nil } +func nokLimitCase() (int, error) { return 0, nil } +func nokMore() (int, int, int, int, error) { return 0, 0, 0, 0, nil } +func nokInvalidReturnInterfaceType() interface{} { return 0 } +func nokInvalidReturnSliceType() []int { return nil } +func nokInvalidReturnOtherType() chan int { return nil } From 63fd657a22836beee73aa748b113e37eb5feb358 Mon Sep 17 00:00:00 2001 From: tfreville Date: Thu, 10 Dec 2020 19:12:59 +0100 Subject: [PATCH 3/4] test: Add test for stepdef model --- internal/models/stepdef.go | 2 +- internal/models/stepdef_test.go | 179 ++++++++++++++++++++++++++++++-- 2 files changed, 169 insertions(+), 12 deletions(-) diff --git a/internal/models/stepdef.go b/internal/models/stepdef.go index 98b0a00..d259359 100644 --- a/internal/models/stepdef.go +++ b/internal/models/stepdef.go @@ -62,7 +62,7 @@ func (sd *StepDefinition) Run() interface{} { if err != nil { return fmt.Errorf(`%w %d: "%s" to int64: %s`, ErrCannotConvert, i, s, err) } - values = append(values, reflect.ValueOf(int64(v))) + values = append(values, reflect.ValueOf(v)) case reflect.Int32: s, err := sd.shouldBeString(i) if err != nil { diff --git a/internal/models/stepdef_test.go b/internal/models/stepdef_test.go index 2e42f02..51bb772 100644 --- a/internal/models/stepdef_test.go +++ b/internal/models/stepdef_test.go @@ -1,6 +1,7 @@ package models_test import ( + "errors" "fmt" "reflect" "strings" @@ -8,6 +9,7 @@ import ( "github.com/cucumber/messages-go/v16" + "github.com/cucumber/godog" "github.com/cucumber/godog/formatters" "github.com/cucumber/godog/internal/models" ) @@ -153,23 +155,178 @@ func TestUnexpectedArguments(t *testing.T) { } def.Args = []interface{}{"1"} - if err := def.Run(); err == nil { + + res := def.Run() + if res == nil { t.Fatalf("expected an error due to wrong number of arguments, but got none") } - def.Args = []interface{}{"one", "two"} - if err := def.Run(); err == nil { - t.Fatalf("expected conversion error, but got none") + err, ok := res.(error) + if !ok { + t.Fatalf("expected an error due to wrong number of arguments, but got %T instead", res) } - // @TODO maybe we should support duration - // fn2 := func(err time.Duration) error { return nil } - // def = &models.StepDefinition{Handler: fn2, HandlerValue: reflect.ValueOf(fn2)} + if !errors.Is(err, models.ErrUnmatchedStepArgumentNumber) { + t.Fatalf("expected an error due to wrong number of arguments, but got %v instead", err) + } +} - // def.Args = []interface{}{"1"} - // if err := def.Run(); err == nil { - // t.Fatalf("expected an error due to wrong argument type, but got none") - // } +func TestStepDefinition_Run_StepShouldBeString(t *testing.T) { + test := func(t *testing.T, fn interface{}) { + def := &models.StepDefinition{ + StepDefinition: formatters.StepDefinition{ + Handler: fn, + }, + HandlerValue: reflect.ValueOf(fn), + } + + def.Args = []interface{}{12} + + res := def.Run() + if res == nil { + t.Fatalf("expected a string convertion error, but got none") + } + + err, ok := res.(error) + if !ok { + t.Fatalf("expected a string convertion error, but got %T instead", res) + } + + if !errors.Is(err, models.ErrCannotConvert) { + t.Fatalf("expected a string convertion error, but got '%v' instead", err) + } + } + + // Ensure step type error if step argument is not a string + // for all supported types. + test(t, func(a int) error { return nil }) + test(t, func(a int64) error { return nil }) + test(t, func(a int32) error { return nil }) + test(t, func(a int16) error { return nil }) + test(t, func(a int8) error { return nil }) + test(t, func(a string) error { return nil }) + test(t, func(a float64) error { return nil }) + test(t, func(a float32) error { return nil }) + test(t, func(a *godog.Table) error { return nil }) + test(t, func(a *godog.DocString) error { return nil }) + test(t, func(a []byte) error { return nil }) + +} + +func TestStepDefinition_Run_InvalidHandlerParamConversion(t *testing.T) { + test := func(t *testing.T, fn interface{}) { + def := &models.StepDefinition{ + StepDefinition: formatters.StepDefinition{ + Handler: fn, + }, + HandlerValue: reflect.ValueOf(fn), + } + + def.Args = []interface{}{12} + + res := def.Run() + if res == nil { + t.Fatalf("expected an unsupported argument type error, but got none") + } + + err, ok := res.(error) + if !ok { + t.Fatalf("expected an unsupported argument type error, but got %T instead", res) + } + + if !errors.Is(err, models.ErrUnsupportedArgumentType) { + t.Fatalf("expected an unsupported argument type error, but got '%v' instead", err) + } + } + + // Lists some unsupported argument types for step handler. + + // Pointers should work only for godog.Table/godog.DocString + test(t, func(a *int) error { return nil }) + test(t, func(a *int64) error { return nil }) + test(t, func(a *int32) error { return nil }) + test(t, func(a *int16) error { return nil }) + test(t, func(a *int8) error { return nil }) + test(t, func(a *string) error { return nil }) + test(t, func(a *float64) error { return nil }) + test(t, func(a *float32) error { return nil }) + + // I cannot pass structures + test(t, func(a godog.Table) error { return nil }) + test(t, func(a godog.DocString) error { return nil }) + test(t, func(a testStruct) error { return nil }) + + // I cannot use maps + test(t, func(a map[string]interface{}) error { return nil }) + test(t, func(a map[string]int) error { return nil }) + + // Slice works only for byte + test(t, func(a []int) error { return nil }) + test(t, func(a []string) error { return nil }) + test(t, func(a []bool) error { return nil }) + + // I cannot use bool + test(t, func(a bool) error { return nil }) + +} + +func TestStepDefinition_Run_StringConversionToFunctionType(t *testing.T) { + test := func(t *testing.T, fn interface{}, args []interface{}) { + def := &models.StepDefinition{ + StepDefinition: formatters.StepDefinition{ + Handler: fn, + }, + HandlerValue: reflect.ValueOf(fn), + Args: args, + } + + res := def.Run() + if res == nil { + t.Fatalf("expected a cannot convert argument type error, but got none") + } + + err, ok := res.(error) + if !ok { + t.Fatalf("expected a cannot convert argument type error, but got %T instead", res) + } + + if !errors.Is(err, models.ErrCannotConvert) { + t.Fatalf("expected a cannot convert argument type error, but got '%v' instead", err) + } + } + + // Lists some unsupported argument types for step handler. + + // Cannot convert invalid int + test(t, func(a int) error { return nil }, []interface{}{"a"}) + test(t, func(a int64) error { return nil }, []interface{}{"a"}) + test(t, func(a int32) error { return nil }, []interface{}{"a"}) + test(t, func(a int16) error { return nil }, []interface{}{"a"}) + test(t, func(a int8) error { return nil }, []interface{}{"a"}) + + // Cannot convert invalid float + test(t, func(a float32) error { return nil }, []interface{}{"a"}) + test(t, func(a float64) error { return nil }, []interface{}{"a"}) + + // Cannot convert to DataArg + test(t, func(a *godog.Table) error { return nil }, []interface{}{"194"}) + + // Cannot convert to DocString ? + test(t, func(a *godog.DocString) error { return nil }, []interface{}{"194"}) + +} + +// @TODO maybe we should support duration +// fn2 := func(err time.Duration) error { return nil } +// def = &models.StepDefinition{Handler: fn2, HandlerValue: reflect.ValueOf(fn2)} + +// def.Args = []interface{}{"1"} +// if err := def.Run(); err == nil { +// t.Fatalf("expected an error due to wrong argument type, but got none") +// } + +type testStruct struct { + a string } func TestShouldSupportDocStringToStringConversion(t *testing.T) { From cdbb0ac3f3662e7eb85c6eb4b547595a9228d02a Mon Sep 17 00:00:00 2001 From: Viacheslav Poturaev Date: Thu, 22 Jul 2021 20:56:26 +0200 Subject: [PATCH 4/4] Update documentation --- CHANGELOG.md | 2 ++ README.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 196911f..b1be2ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt ### Added +* Support for step definitions without return ([364](https://github.com/cucumber/godog/pull/364) -[titouanfreville]) + ### Changed * Upgraded gherkin-go to v19 ([402](https://github.com/cucumber/godog/pull/402) - [mbow]) diff --git a/README.md b/README.md index d5ce43d..9a3e3a2 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,14 @@ Feature: eat godogs You may change **return godog.ErrPending** to **return nil** in the three step definitions and the scenario will pass successfully. +Also, you may omit error return if your step does not fail. + +```go +func iEat(arg1 int) { + // Eat arg1. +} +``` + #### Step 5 - Create the main program to test We only need a number of **godogs** for now. Lets keep it simple.