Replies: 1 comment
-
Why not? "Magic Comments" are called directives and are a core and common part of the language. When we combine them with codegen you can get really clean step definitions: /* file: godog_steps.go */
//go:generate go run github.com/lukasngl/godogen
package godogs
//godog:when ^I eat (\d+)$
func iEat(ctx context.Context, num int) (context.Context, error) {
// … get the available value from context etc.
available -= num
return context.WithValue(ctx, godogsCtxKey{}, available), nil
} for which we can generate code like this: /* file: godog_steps_initializer.go */
package godogs
import "github.com/cucumber/godog"
// InitializeGodogSteps registers steps defined in "godog_steps.go" with the [godog.ScenarioContext].
func InitializeGodogSteps(ctx *godog.ScenarioContext) {
ctx.Step(`^I eat (\d+)$`, iEat)
} Have a look at godogen where I implemented the codegen. I got a bit carried away and also add a linting, so maybe have a look at the simple version. Arguably the lining is not 100% necessary as it is caught when running the tests, but paired with golangci-lint in your editor it provides nice and early feedback. As a limitation, you can currently only use functions, i.e. not methods, as steps and have to wire everything through context.Context. I personally think that this is cleaner and better for concurrency anyways, since the context is scenario scoped, and a Suite struct would be test scoped. As an upside, they are just functions so you can use them in "normal" go tests, see https://go.dev/play/p/3iyUoYczGTf If you don't want codegen, you could achieve something similar by doing something like this: package godog_test
import (
"context"
"fmt"
"github.com/cucumber/godog"
)
func InitializeScenario(ctx *godog.ScenarioContext) {
for pattern, step := range steps {
ctx.Step(pattern, step)
}
}
// steps
var _ = registerStep(`^i eat (\d+) godogs$`, func(ctx context.Context, num int) (context.Context, error) {
availableGodogs := ctx.Value(gogodCtxKey{}).(int)
if availableGodogs < num {
return ctx, fmt.Errorf("can't eat more than available")
}
return context.WithValue(ctx, gogodCtxKey{}, availableGodogs-num), nil
},
)
// -- or --
var _ = registerStep(`^i have (\d+) godogs$`, iHave)
func iHave(ctx context.Context, expected int) error {
actualGodogs := ctx.Value(gogodCtxKey{}).(int)
if actualGodogs != expected {
return fmt.Errorf("expected %d godogs, but got %d", expected, actualGodogs)
}
return nil
}
// infra
var steps map[string]any
// TODO: given, when, then
func registerStep(pattern, any) {
if steps == nil {
steps := make(map[string]any)
}
steps[pattern] = step
} Just duplicate it for WDYT? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
While investigating data for the poll #542, it became clear that
godog
(as the de-facto BDD library forgo
) seems to be the only one that splits the step regex and the step function. It seems that every other language has a nice way of adding metadata to functions and this metadata then can contains the regex.Why not do it in
go
?We can't add metadata to functions like other languages but we can wrap functions in other functions that can then add that metadata. We could use "magic comments", but let's not even start on that…
I'm talking about this:
I'm sure there were good design decisions made at the time that caused this split. But can we do better now?
Would it make maintenance and discoverability easier and probably some other software quality metrics much better too? In fact, I have already done it as part of PoC to see if/how this would be possible. It's not difficult, but there is some boilerplate which (in the ideal world…) maybe could make its way into godog directly.
I'd love to discuss this approach and "if it has legs" then perhaps start looking into if it could become the default pattern for
godog
. This seems to be like a long shot and quite ambitious goal, but why not?My example step definition now looks like this:
As you can see, not only the regex is right next to the func implementation, but this approach also allows me to remove a lot of code complexity and boilerplate that does getting/setting context values. All with roughly the same amount of lines of code.
WDYT?
Beta Was this translation helpful? Give feedback.
All reactions