Skip to content

Commit

Permalink
Add support for loading features from fs.FS (#146)
Browse files Browse the repository at this point in the history
  • Loading branch information
sagikazarmark authored Aug 23, 2022
1 parent 359187c commit 2911ec3
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 15 deletions.
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) {
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()
}

0 comments on commit 2911ec3

Please sign in to comment.