Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for loading features from fs.FS #146

Merged
merged 9 commits into from
Aug 23, 2022
17 changes: 17 additions & 0 deletions docs/suite-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The suite can be confiugred using one of these functions:

* `RunInParallel()` - enables running steps in parallel. It uses the stanard `T.Parallel` function.
* `WithFeaturesPath(path string)` - configures the path where GoBDD should look for features. The default value is `features/*.feature`.
* `WithFeaturesFS(fs fs.FS, path string)` - configures the filesystem and a path (glob pattern) where GoBDD should look for features.
* `WithTags(tags ...string)` - configures which tags should be run. Every tag has to start with `@`.
* `WithBeforeScenario(f func())` - this function `f` will be called before every scenario.
* `WithAfterScenario(f func())` - this funcion `f` will be called after every scenario.
Expand All @@ -26,3 +27,19 @@ suite := NewSuite(t, WithFeaturesPath("features/func_types.feature"))
suite := NewSuite(t, WithFeaturesPath("features/tags.feature"), WithTags([]string{"@tag"}))
```

As of Go 1.16 you can embed feature files into the test binary and use `fs.FS` as a feature source:

```go
import (
"embed"
)

//go:embed features/*.feature
var featuresFS embed.FS

// ...

suite := NewSuite(t, WithFeaturesFS(featuresFS, "*.feature"))
```

While in most cases it doesn't make any difference, embedding feature files makes your tests more portable.
11 changes: 11 additions & 0 deletions features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build go1.16
// +build go1.16

package gobdd

import (
"embed"
)

//go:embed features/*.feature
var featuresFS embed.FS
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/go-bdd/gobdd

go 1.12
go 1.16

require (
github.com/cucumber/gherkin-go/v13 v13.0.0
Expand Down
68 changes: 54 additions & 14 deletions gobdd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
Expand All @@ -27,7 +28,7 @@ type Suite struct {

// SuiteOptions holds all the information about how the suite or features/steps should be configured
type SuiteOptions struct {
featuresPaths string
featureSource featureSource
ignoreTags []string
tags []string
beforeScenario []func(ctx Context)
Expand All @@ -37,10 +38,46 @@ type SuiteOptions struct {
runInParallel bool
}

type featureSource interface {
loadFeatures() ([]feature, error)
}

type feature interface {
Open() (io.Reader, error)
}

type pathFeatureSource string

func (s pathFeatureSource) loadFeatures() ([]feature, error) {
files, err := filepath.Glob(string(s))
if err != nil {
return nil, errors.New("cannot find features/ directory")
}

features := make([]feature, 0, len(files))

for _, f := range files {
features = append(features, fileFeature(f))
}

return features, nil
}

type fileFeature string

func (f fileFeature) Open() (io.Reader, error) {
file, err := os.Open(string(f))
if err != nil {
return nil, fmt.Errorf("cannot open file %s", f)
}

return file, nil
}

// NewSuiteOptions creates a new suite configuration with default values
func NewSuiteOptions() SuiteOptions {
return SuiteOptions{
featuresPaths: "features/*.feature",
featureSource: pathFeatureSource("features/*.feature"),
ignoreTags: []string{},
tags: []string{},
beforeScenario: []func(ctx Context){},
Expand All @@ -61,7 +98,7 @@ func RunInParallel() func(*SuiteOptions) {
// The default value is "features/*.feature"
func WithFeaturesPath(path string) func(*SuiteOptions) {
return func(options *SuiteOptions) {
options.featuresPaths = path
options.featureSource = pathFeatureSource(path)
}
}

Expand Down Expand Up @@ -141,7 +178,6 @@ type FeatureKey struct{}
// ScenarioKey is used to store reference to current *msgs.GherkinDocument_Feature_Scenario instance
type ScenarioKey struct{}


// Creates a new suites with given configuration and empty steps defined
func NewSuite(t TestingT, optionClosures ...func(*SuiteOptions)) *Suite {
options := NewSuiteOptions()
Expand Down Expand Up @@ -268,32 +304,36 @@ func (s *Suite) Run() {
return
}

files, err := filepath.Glob(s.options.featuresPaths)
features, err := s.options.featureSource.loadFeatures()
if err != nil {
s.t.Fatalf("cannot find features/ directory")
s.t.Fatalf(err.Error())
}

if s.options.runInParallel {
s.t.Parallel()
}

for _, file := range files {
err = s.executeFeature(file)
for _, feature := range features {
err = s.executeFeature(feature)
if err != nil {
s.t.Fail()
}
}
}

func (s *Suite) executeFeature(file string) error {
f, err := os.Open(file)
func (s *Suite) executeFeature(feature feature) error {
f, err := feature.Open()
if err != nil {
return fmt.Errorf("cannot open file %s", file)
return err
}
defer f.Close()
fileIO := bufio.NewReader(f)

doc, err := gherkin.ParseGherkinDocument(fileIO, (&msgs.Incrementing{}).NewId)
if closer, ok := f.(io.Closer); ok {
defer closer.Close()
}

featureIO := bufio.NewReader(f)

doc, err := gherkin.ParseGherkinDocument(featureIO, (&msgs.Incrementing{}).NewId)
if err != nil {
s.t.Fatalf("error while loading document: %s\n", err)
}
Expand Down
57 changes: 57 additions & 0 deletions gobdd_go1_16.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//go:build go1.16
// +build go1.16

package gobdd

import (
"fmt"
"io"
"io/fs"
)

// WithFeaturesFS configures a filesystem and a path (glob pattern) where features can be found.
func WithFeaturesFS(fs fs.FS, path string) func(*SuiteOptions) {
return func(options *SuiteOptions) {
options.featureSource = fsFeatureSource{
fs: fs,
path: path,
}
}
}

type fsFeatureSource struct {
fs fs.FS
path string
}

func (s fsFeatureSource) loadFeatures() ([]feature, error) {
files, err := fs.Glob(s.fs, s.path)
if err != nil {
return nil, fmt.Errorf("loading features: %w", err)
}

features := make([]feature, 0, len(files))

for _, f := range files {
features = append(features, fsFeature{
fs: s.fs,
file: f,
})
}

return features, nil
}

type fsFeature struct {
fs fs.FS
file string
}

func (f fsFeature) Open() (io.Reader, error) {
file, err := f.fs.Open(f.file)
if err != nil {
return nil, fmt.Errorf("opening feature: %w", err)
}

return file, nil
}
19 changes: 19 additions & 0 deletions gobdd_go1_16_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build go1.16
// +build go1.16

package gobdd

import (
"regexp"
"testing"
)

func TestWithFeaturesFS(t *testing.T) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any ideas for better tests?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the test is OK. I have some thoughts about the API (see commend below).

suite := NewSuite(t, WithFeaturesFS(featuresFS, "example.feature"))
compiled := regexp.MustCompile(`I add (\d+) and (\d+)`)
suite.AddRegexStep(compiled, add)
compiled = regexp.MustCompile(`the result should equal (\d+)`)
suite.AddRegexStep(compiled, check)

suite.Run()
}