├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ ├── ci.yml
│ └── greetings.yml
├── .gitignore
├── .golangci.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── context.go
├── context
└── context.go
├── context_get.go
├── context_get_as_test.go
├── context_get_test.go
├── context_test.go
├── doc.go
├── docs
├── 404.html
├── Gemfile
├── Gemfile.lock
├── _config.yml
├── _data
│ └── navigation.yml
├── archives.md
├── context.md
├── creating-steps.markdown
├── docker-compose.yml
├── feed.xml
├── index.md
├── parameter-types.md
├── quick-start.markdown
└── suite-options.md
├── features.go
├── features
├── argument.feature
├── background.feature
├── datatable.feature
├── empty.feature
├── example.feature
├── example_rule.feature
├── filter_tags_negative.feature
├── filter_tags_positive.feature
├── func_types.feature
├── http.feature
├── ignored_feature_tags.feature
├── ignored_rule_tags.feature
├── ignored_tags.feature
├── outline.feature
├── parameter-types.feature
└── tags.feature
├── gen.go
├── gen
└── ctxgen.go
├── go.mod
├── go.sum
├── gobdd.go
├── gobdd_go1_16.go
├── gobdd_go1_16_test.go
├── gobdd_test.go
├── steps.go
├── steps_context_test.go
└── steps_test.go
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: go-bdd
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 |
15 | **Expected behavior**
16 |
17 | **Additional context**
18 | Add any other context about the problem here.
19 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 |
10 | jobs:
11 | golangci:
12 | name: Lint
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v2
17 |
18 | - name: Lint
19 | uses: golangci/golangci-lint-action@v3
20 | with:
21 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
22 | version: v1.63
23 |
24 | coverage:
25 | name: Build
26 | runs-on: ubuntu-latest
27 | strategy:
28 | matrix:
29 | go: [ '1.17', '1.18', '1.19', '1.20', '1.21', '1.22', '1.23' ]
30 | env:
31 | GOFLAGS: -mod=readonly
32 | GOPROXY: https://proxy.golang.org
33 |
34 | steps:
35 | - name: Set up Go
36 | uses: actions/setup-go@v3
37 | with:
38 | go-version: ${{ matrix.go }}
39 |
40 | - name: Checkout code
41 | uses: actions/checkout@v3
42 |
43 | - name: go generate
44 | run: go generate
45 |
46 | - name: Run tests
47 | run: go test ./...
48 |
49 | - name: Calc coverage
50 | run: |
51 | export PATH=$PATH:$(go env GOPATH)/bin
52 | go test -v -covermode=count -coverprofile=coverage.out
53 |
54 | - name: Convert coverage to lcov
55 | uses: jandelgado/gcov2lcov-action@v1
56 | with:
57 | infile: coverage.out
58 | outfile: coverage.lcov
59 |
60 | - name: Coveralls
61 | uses: coverallsapp/github-action@v1
62 | with:
63 | github-token: ${{ secrets.github_token }}
64 | path-to-lcov: coverage.lcov
65 |
--------------------------------------------------------------------------------
/.github/workflows/greetings.yml:
--------------------------------------------------------------------------------
1 | name: Greetings
2 |
3 | on: [pull_request, issues]
4 |
5 | jobs:
6 | greeting:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/first-interaction@v1
10 | with:
11 | repo-token: ${{ secrets.GITHUB_TOKEN }}
12 | issue-message: 'Thanks for creating your first issue! We are thankful for your help'
13 | pr-message: 'Thanks for submitting your first PR! We are thankful for your help'
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | *.idea/
8 |
9 | # Test binary, build with `go test -c`
10 | *.test
11 |
12 | # Output of the go coverage tool, specifically when used with LiteIDE
13 | *.out
14 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | xlinters-settings:
2 | golint:
3 | min-confidence: 0.1
4 |
5 | linters:
6 | # TODO: best practice is to enable specific linters
7 | enable-all: true
8 | disable:
9 | - funlen
10 | - err113
11 | - gofumpt
12 | - godot
13 | - testpackage
14 | - nolintlint
15 | - gosec
16 | - depguard
17 |
18 | # Disabled after upgrading golangci-lint
19 | # Enable them after issues are resolved
20 | - gofmt
21 | - goimports
22 | - exhaustruct
23 | - forcetypeassert
24 | - gosimple
25 | - nlreturn
26 | - paralleltest
27 | - thelper
28 | - varnamelen
29 | - wrapcheck
30 | - wsl
31 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at bklimczak@developer20.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, please first discuss the change you wish to make via issue,
4 | email, or any other method with the owners of this repository before making a change.
5 |
6 | Please note we have a code of conduct, please follow it in all your interactions with the project.
7 |
8 | ## Pull Request Process
9 |
10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a
11 | build.
12 | 2. Update the README.md with details of changes to the interface, this includes new environment
13 | variables, exposed ports, useful file locations and container parameters.
14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this
15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
17 | do not have permission to do that, you may request the second reviewer to merge it for you.
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Bartłomiej Klimczak
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GOBDD
2 |
3 | [](https://godoc.org/github.com/go-bdd/gobdd) [](https://coveralls.io/github/go-bdd/gobdd?branch=master)
4 |
5 | This is a BDD testing framework. Uses gherkin for the test's syntax. From version 1.0, the API is stable.
6 |
7 | ## Why did I make the library?
8 |
9 | There is godog library for BDD tests in Go. I found this library useful but it runs as an external application which compiles our code. It has several disadvantages:
10 |
11 | - no debugging (breakpoints) in the test. Sometimes it’s useful to go through the whole execution step by step
12 | - metrics don’t count the test run this way
13 | - some style checkers recognise tests as dead code
14 | - it’s impossible to use built-in features like build constraints.
15 | - no context in steps - so the state have to be stored somewhere else - in my opinion, it makes the maintenance harder
16 |
17 | ## Quick start
18 |
19 | Add the package to your project:
20 |
21 | ```
22 | go install github.com/go-bdd/gobdd@latest
23 | ```
24 |
25 | Inside `features` folder create your scenarios. Here is an example:
26 |
27 | ```gherkin
28 | Feature: math operations
29 | Scenario: add two digits
30 | When I add 1 and 2
31 | Then the result should equal 3
32 | ```
33 |
34 | Add a new test `main_test.go`:
35 |
36 | ```go
37 | func add(t gobdd.StepTest, ctx gobdd.Context, var1, var2 int) {
38 | res := var1 + var2
39 | ctx.Set("sumRes", res)
40 | }
41 |
42 | func check(t gobdd.StepTest, ctx gobdd.Context, sum int) {
43 | received, err := ctx.GetInt("sumRes")
44 | if err != nil {
45 | t.Error(err)
46 | return
47 | }
48 |
49 | if sum != received {
50 | t.Error(errors.New("the math does not work for you"))
51 | }
52 | }
53 |
54 | func TestScenarios(t *testing.T) {
55 | suite := gobdd.NewSuite(t)
56 | suite.AddStep(`I add (\d+) and (\d+)`, add)
57 | suite.AddStep(`the result should equal (\d+)`, check)
58 | suite.Run()
59 | }
60 | ```
61 |
62 | and run tests
63 |
64 | ```
65 | go test ./...
66 | ```
67 |
68 | More detailed documentation can be found on the docs page: https://go-bdd.github.io/gobdd/. A sample application is available in [a separate repository](https://github.com/go-bdd/sample-app).
69 |
70 | # Contributing
71 |
72 | All contributions are very much welcome. If you'd like to help with GoBDD development, please see open issues and submit your pull request via GitHub.
73 |
74 | # Support
75 |
76 | If you didn't find the answer to your question in the documentation, feel free to ask us directly!
77 |
78 | Please join us on the `#gobdd-library` channel on the [Gophers slack](https://gophers.slack.com/): You can get [an invite here](https://gophersinvite.herokuapp.com/).
79 | You can find updates about the progress on Twitter: [GoBdd](https://twitter.com/Go_BDD).
80 |
81 | You can support my work using [issuehunt](https://issuehunt.io/r/go-bdd) or by [buying me a coffee](https://www.buymeacoffee.com/bklimczak).
82 |
--------------------------------------------------------------------------------
/context.go:
--------------------------------------------------------------------------------
1 | package gobdd
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | )
7 |
8 | // Holds data from previously executed steps
9 | type Context struct {
10 | values map[interface{}]interface{}
11 | }
12 |
13 | // Creates a new (empty) context struct
14 | func NewContext() Context {
15 | return Context{
16 | values: map[interface{}]interface{}{},
17 | }
18 | }
19 |
20 | // Clone creates a copy of the context
21 | func (ctx Context) Clone() Context {
22 | c := Context{
23 | values: map[interface{}]interface{}{},
24 | }
25 |
26 | for k, v := range ctx.values {
27 | c.Set(k, v)
28 | }
29 |
30 | return c
31 | }
32 |
33 | // Sets the value under the key
34 | func (ctx Context) Set(key interface{}, value interface{}) {
35 | ctx.values[key] = value
36 | }
37 |
38 | // Returns the data under the key.
39 | // If couldn't find anything but the default value is provided, returns the default value.
40 | // Otherwise, it returns an error.
41 | func (ctx Context) Get(key interface{}, defaultValue ...interface{}) (interface{}, error) {
42 | if _, ok := ctx.values[key]; !ok {
43 | if len(defaultValue) == 1 {
44 | return defaultValue[0], nil
45 | }
46 |
47 | return nil, fmt.Errorf("the key %+v does not exist", key)
48 | }
49 |
50 | return ctx.values[key], nil
51 | }
52 |
53 | // GetAs copies data from tke key to the dest.
54 | // Supports maps, slices and structs.
55 | func (ctx Context) GetAs(key interface{}, dest interface{}) error {
56 | if _, ok := ctx.values[key]; !ok {
57 | return fmt.Errorf("the key %+v does not exist", key)
58 | }
59 |
60 | d, err := json.Marshal(ctx.values[key])
61 | if err != nil {
62 | return err
63 | }
64 |
65 | return json.Unmarshal(d, dest)
66 | }
67 |
68 | // It is a shortcut for getting the value already casted as error.
69 | func (ctx Context) GetError(key interface{}, defaultValue ...error) (error, error) {
70 | if _, ok := ctx.values[key]; !ok {
71 | if len(defaultValue) == 1 {
72 | return defaultValue[0], nil
73 | }
74 |
75 | return nil, fmt.Errorf("the key %+v does not exist", key)
76 | }
77 |
78 | if ctx.values[key] == nil {
79 | return nil, nil // nolint:nilnil
80 | }
81 |
82 | value, ok := ctx.values[key].(error)
83 | if !ok {
84 | return nil, fmt.Errorf("the expected value is not error (%T)", key)
85 | }
86 |
87 | return value, nil
88 | }
89 |
--------------------------------------------------------------------------------
/context/context.go:
--------------------------------------------------------------------------------
1 | package context
2 |
3 | import (
4 | gobdd "github.com/go-bdd/gobdd"
5 | )
6 |
7 | // Holds data from previously executed steps
8 | // Deprecated: use gobdd.Context instead.
9 | type Context = gobdd.Context
10 |
11 | // Creates a new (empty) context struct
12 | // Deprecated: use gobdd.NewContext instead.
13 | func New() Context {
14 | return gobdd.NewContext()
15 | }
16 |
--------------------------------------------------------------------------------
/context_get.go:
--------------------------------------------------------------------------------
1 | // Code generated .* DO NOT EDIT.
2 | package gobdd
3 |
4 | import "fmt"
5 |
6 |
7 | func (ctx Context) GetString(key interface{}, defaultValue ...string) (string, error) {
8 | if len(defaultValue) > 1 {
9 | return "", fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
10 | }
11 |
12 | if _, ok := ctx.values[key]; !ok {
13 | if len(defaultValue) == 1 {
14 | return defaultValue[0], nil
15 | }
16 | return "", fmt.Errorf("the key %+v does not exist", key)
17 | }
18 |
19 | value, ok := ctx.values[key].(string)
20 | if !ok {
21 | return "", fmt.Errorf("the expected value is not string (%T)", key)
22 | }
23 | return value, nil
24 | }
25 |
26 | func (ctx Context) GetInt(key interface{}, defaultValue ...int) (int, error) {
27 | if len(defaultValue) > 1 {
28 | return 0, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
29 | }
30 |
31 | if _, ok := ctx.values[key]; !ok {
32 | if len(defaultValue) == 1 {
33 | return defaultValue[0], nil
34 | }
35 | return 0, fmt.Errorf("the key %+v does not exist", key)
36 | }
37 |
38 | value, ok := ctx.values[key].(int)
39 | if !ok {
40 | return 0, fmt.Errorf("the expected value is not int (%T)", key)
41 | }
42 | return value, nil
43 | }
44 |
45 | func (ctx Context) GetInt8(key interface{}, defaultValue ...int8) (int8, error) {
46 | if len(defaultValue) > 1 {
47 | return 0, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
48 | }
49 |
50 | if _, ok := ctx.values[key]; !ok {
51 | if len(defaultValue) == 1 {
52 | return defaultValue[0], nil
53 | }
54 | return 0, fmt.Errorf("the key %+v does not exist", key)
55 | }
56 |
57 | value, ok := ctx.values[key].(int8)
58 | if !ok {
59 | return 0, fmt.Errorf("the expected value is not int8 (%T)", key)
60 | }
61 | return value, nil
62 | }
63 |
64 | func (ctx Context) GetInt16(key interface{}, defaultValue ...int16) (int16, error) {
65 | if len(defaultValue) > 1 {
66 | return 0, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
67 | }
68 |
69 | if _, ok := ctx.values[key]; !ok {
70 | if len(defaultValue) == 1 {
71 | return defaultValue[0], nil
72 | }
73 | return 0, fmt.Errorf("the key %+v does not exist", key)
74 | }
75 |
76 | value, ok := ctx.values[key].(int16)
77 | if !ok {
78 | return 0, fmt.Errorf("the expected value is not int16 (%T)", key)
79 | }
80 | return value, nil
81 | }
82 |
83 | func (ctx Context) GetInt32(key interface{}, defaultValue ...int32) (int32, error) {
84 | if len(defaultValue) > 1 {
85 | return 0, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
86 | }
87 |
88 | if _, ok := ctx.values[key]; !ok {
89 | if len(defaultValue) == 1 {
90 | return defaultValue[0], nil
91 | }
92 | return 0, fmt.Errorf("the key %+v does not exist", key)
93 | }
94 |
95 | value, ok := ctx.values[key].(int32)
96 | if !ok {
97 | return 0, fmt.Errorf("the expected value is not int32 (%T)", key)
98 | }
99 | return value, nil
100 | }
101 |
102 | func (ctx Context) GetInt64(key interface{}, defaultValue ...int64) (int64, error) {
103 | if len(defaultValue) > 1 {
104 | return 0, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
105 | }
106 |
107 | if _, ok := ctx.values[key]; !ok {
108 | if len(defaultValue) == 1 {
109 | return defaultValue[0], nil
110 | }
111 | return 0, fmt.Errorf("the key %+v does not exist", key)
112 | }
113 |
114 | value, ok := ctx.values[key].(int64)
115 | if !ok {
116 | return 0, fmt.Errorf("the expected value is not int64 (%T)", key)
117 | }
118 | return value, nil
119 | }
120 |
121 | func (ctx Context) GetFloat32(key interface{}, defaultValue ...float32) (float32, error) {
122 | if len(defaultValue) > 1 {
123 | return 0, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
124 | }
125 |
126 | if _, ok := ctx.values[key]; !ok {
127 | if len(defaultValue) == 1 {
128 | return defaultValue[0], nil
129 | }
130 | return 0, fmt.Errorf("the key %+v does not exist", key)
131 | }
132 |
133 | value, ok := ctx.values[key].(float32)
134 | if !ok {
135 | return 0, fmt.Errorf("the expected value is not float32 (%T)", key)
136 | }
137 | return value, nil
138 | }
139 |
140 | func (ctx Context) GetFloat64(key interface{}, defaultValue ...float64) (float64, error) {
141 | if len(defaultValue) > 1 {
142 | return 0, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
143 | }
144 |
145 | if _, ok := ctx.values[key]; !ok {
146 | if len(defaultValue) == 1 {
147 | return defaultValue[0], nil
148 | }
149 | return 0, fmt.Errorf("the key %+v does not exist", key)
150 | }
151 |
152 | value, ok := ctx.values[key].(float64)
153 | if !ok {
154 | return 0, fmt.Errorf("the expected value is not float64 (%T)", key)
155 | }
156 | return value, nil
157 | }
158 |
159 | func (ctx Context) GetBool(key interface{}, defaultValue ...bool) (bool, error) {
160 | if len(defaultValue) > 1 {
161 | return false, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
162 | }
163 |
164 | if _, ok := ctx.values[key]; !ok {
165 | if len(defaultValue) == 1 {
166 | return defaultValue[0], nil
167 | }
168 | return false, fmt.Errorf("the key %+v does not exist", key)
169 | }
170 |
171 | value, ok := ctx.values[key].(bool)
172 | if !ok {
173 | return false, fmt.Errorf("the expected value is not bool (%T)", key)
174 | }
175 | return value, nil
176 | }
177 |
178 |
--------------------------------------------------------------------------------
/context_get_as_test.go:
--------------------------------------------------------------------------------
1 | package gobdd
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestContext_GetAs_NoPointerType(t *testing.T) {
10 | s := []string{"one", "two"}
11 | res := ""
12 |
13 | ctx := NewContext()
14 | ctx.Set("s", s)
15 | err := ctx.GetAs("s", res)
16 | require.Error(t, err)
17 | }
18 |
19 | func TestContext_GetAs_WithSlice(t *testing.T) {
20 | s := []string{"one", "two"}
21 | res := []string{}
22 |
23 | ctx := NewContext()
24 | ctx.Set("s", s)
25 | err := ctx.GetAs("s", &res)
26 | require.NoError(t, err)
27 | require.Equal(t, s, res)
28 | }
29 |
30 | func TestContext_GetAs_WithMap(t *testing.T) {
31 | m := map[string]string{}
32 | res := map[string]string{}
33 |
34 | m["key"] = "value"
35 | ctx := NewContext()
36 | ctx.Set("map", m)
37 | err := ctx.GetAs("map", &res)
38 | require.NoError(t, err)
39 | require.Equal(t, m, res)
40 | }
41 |
--------------------------------------------------------------------------------
/context_get_test.go:
--------------------------------------------------------------------------------
1 | // Code generated .* DO NOT EDIT.
2 | package gobdd
3 |
4 | import "testing"
5 | import "errors"
6 |
7 | func TestContext_GetError(t *testing.T) {
8 | ctx := NewContext()
9 | expected := errors.New("new err")
10 | ctx.Set("test", expected)
11 | received, err := ctx.GetError("test")
12 | if err != nil {
13 | t.Error(err)
14 | }
15 | if received != expected {
16 | t.Errorf("expected %+v but received %+v", expected, received)
17 | }
18 | }
19 |
20 |
21 | func TestContext_GetString(t *testing.T) {
22 | ctx := NewContext()
23 | expected := string("example text")
24 | ctx.Set("test", expected)
25 | received, err := ctx.GetString("test")
26 | if err != nil {
27 | t.Error(err)
28 | }
29 | if received != expected {
30 | t.Errorf("expected %+v but received %+v", expected, received)
31 | }
32 | }
33 |
34 | func TestContext_GetString_WithDefaultValue(t *testing.T) {
35 | ctx := NewContext()
36 | defaultValue := string("example text")
37 | received, err := ctx.GetString("test", defaultValue)
38 | if err != nil {
39 | t.Error(err)
40 | }
41 | if received != defaultValue {
42 | t.Errorf("expected %+v but received %+v", defaultValue, received)
43 | }
44 | }
45 |
46 | func TestContext_GetString_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
47 | ctx := NewContext()
48 | _, err := ctx.GetString("test", "example text", "example text")
49 | if err == nil {
50 | t.Error("the GetString should return an error")
51 | }
52 | }
53 |
54 | func TestContext_GetString_ErrorOnNotFound(t *testing.T) {
55 | ctx := NewContext()
56 | _, err := ctx.GetString("test")
57 | if err == nil {
58 | t.Error("the GetString should return an error")
59 | }
60 | }
61 |
62 | func TestContext_GetInt(t *testing.T) {
63 | ctx := NewContext()
64 | expected := int(123)
65 | ctx.Set("test", expected)
66 | received, err := ctx.GetInt("test")
67 | if err != nil {
68 | t.Error(err)
69 | }
70 | if received != expected {
71 | t.Errorf("expected %+v but received %+v", expected, received)
72 | }
73 | }
74 |
75 | func TestContext_GetInt_WithDefaultValue(t *testing.T) {
76 | ctx := NewContext()
77 | defaultValue := int(123)
78 | received, err := ctx.GetInt("test", defaultValue)
79 | if err != nil {
80 | t.Error(err)
81 | }
82 | if received != defaultValue {
83 | t.Errorf("expected %+v but received %+v", defaultValue, received)
84 | }
85 | }
86 |
87 | func TestContext_GetInt_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
88 | ctx := NewContext()
89 | _, err := ctx.GetInt("test", 123, 123)
90 | if err == nil {
91 | t.Error("the GetInt should return an error")
92 | }
93 | }
94 |
95 | func TestContext_GetInt_ErrorOnNotFound(t *testing.T) {
96 | ctx := NewContext()
97 | _, err := ctx.GetInt("test")
98 | if err == nil {
99 | t.Error("the GetInt should return an error")
100 | }
101 | }
102 |
103 | func TestContext_GetInt8(t *testing.T) {
104 | ctx := NewContext()
105 | expected := int8(123)
106 | ctx.Set("test", expected)
107 | received, err := ctx.GetInt8("test")
108 | if err != nil {
109 | t.Error(err)
110 | }
111 | if received != expected {
112 | t.Errorf("expected %+v but received %+v", expected, received)
113 | }
114 | }
115 |
116 | func TestContext_GetInt8_WithDefaultValue(t *testing.T) {
117 | ctx := NewContext()
118 | defaultValue := int8(123)
119 | received, err := ctx.GetInt8("test", defaultValue)
120 | if err != nil {
121 | t.Error(err)
122 | }
123 | if received != defaultValue {
124 | t.Errorf("expected %+v but received %+v", defaultValue, received)
125 | }
126 | }
127 |
128 | func TestContext_GetInt8_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
129 | ctx := NewContext()
130 | _, err := ctx.GetInt8("test", 123, 123)
131 | if err == nil {
132 | t.Error("the GetInt8 should return an error")
133 | }
134 | }
135 |
136 | func TestContext_GetInt8_ErrorOnNotFound(t *testing.T) {
137 | ctx := NewContext()
138 | _, err := ctx.GetInt8("test")
139 | if err == nil {
140 | t.Error("the GetInt8 should return an error")
141 | }
142 | }
143 |
144 | func TestContext_GetInt16(t *testing.T) {
145 | ctx := NewContext()
146 | expected := int16(123)
147 | ctx.Set("test", expected)
148 | received, err := ctx.GetInt16("test")
149 | if err != nil {
150 | t.Error(err)
151 | }
152 | if received != expected {
153 | t.Errorf("expected %+v but received %+v", expected, received)
154 | }
155 | }
156 |
157 | func TestContext_GetInt16_WithDefaultValue(t *testing.T) {
158 | ctx := NewContext()
159 | defaultValue := int16(123)
160 | received, err := ctx.GetInt16("test", defaultValue)
161 | if err != nil {
162 | t.Error(err)
163 | }
164 | if received != defaultValue {
165 | t.Errorf("expected %+v but received %+v", defaultValue, received)
166 | }
167 | }
168 |
169 | func TestContext_GetInt16_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
170 | ctx := NewContext()
171 | _, err := ctx.GetInt16("test", 123, 123)
172 | if err == nil {
173 | t.Error("the GetInt16 should return an error")
174 | }
175 | }
176 |
177 | func TestContext_GetInt16_ErrorOnNotFound(t *testing.T) {
178 | ctx := NewContext()
179 | _, err := ctx.GetInt16("test")
180 | if err == nil {
181 | t.Error("the GetInt16 should return an error")
182 | }
183 | }
184 |
185 | func TestContext_GetInt32(t *testing.T) {
186 | ctx := NewContext()
187 | expected := int32(123)
188 | ctx.Set("test", expected)
189 | received, err := ctx.GetInt32("test")
190 | if err != nil {
191 | t.Error(err)
192 | }
193 | if received != expected {
194 | t.Errorf("expected %+v but received %+v", expected, received)
195 | }
196 | }
197 |
198 | func TestContext_GetInt32_WithDefaultValue(t *testing.T) {
199 | ctx := NewContext()
200 | defaultValue := int32(123)
201 | received, err := ctx.GetInt32("test", defaultValue)
202 | if err != nil {
203 | t.Error(err)
204 | }
205 | if received != defaultValue {
206 | t.Errorf("expected %+v but received %+v", defaultValue, received)
207 | }
208 | }
209 |
210 | func TestContext_GetInt32_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
211 | ctx := NewContext()
212 | _, err := ctx.GetInt32("test", 123, 123)
213 | if err == nil {
214 | t.Error("the GetInt32 should return an error")
215 | }
216 | }
217 |
218 | func TestContext_GetInt32_ErrorOnNotFound(t *testing.T) {
219 | ctx := NewContext()
220 | _, err := ctx.GetInt32("test")
221 | if err == nil {
222 | t.Error("the GetInt32 should return an error")
223 | }
224 | }
225 |
226 | func TestContext_GetInt64(t *testing.T) {
227 | ctx := NewContext()
228 | expected := int64(123)
229 | ctx.Set("test", expected)
230 | received, err := ctx.GetInt64("test")
231 | if err != nil {
232 | t.Error(err)
233 | }
234 | if received != expected {
235 | t.Errorf("expected %+v but received %+v", expected, received)
236 | }
237 | }
238 |
239 | func TestContext_GetInt64_WithDefaultValue(t *testing.T) {
240 | ctx := NewContext()
241 | defaultValue := int64(123)
242 | received, err := ctx.GetInt64("test", defaultValue)
243 | if err != nil {
244 | t.Error(err)
245 | }
246 | if received != defaultValue {
247 | t.Errorf("expected %+v but received %+v", defaultValue, received)
248 | }
249 | }
250 |
251 | func TestContext_GetInt64_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
252 | ctx := NewContext()
253 | _, err := ctx.GetInt64("test", 123, 123)
254 | if err == nil {
255 | t.Error("the GetInt64 should return an error")
256 | }
257 | }
258 |
259 | func TestContext_GetInt64_ErrorOnNotFound(t *testing.T) {
260 | ctx := NewContext()
261 | _, err := ctx.GetInt64("test")
262 | if err == nil {
263 | t.Error("the GetInt64 should return an error")
264 | }
265 | }
266 |
267 | func TestContext_GetFloat32(t *testing.T) {
268 | ctx := NewContext()
269 | expected := float32(123.5)
270 | ctx.Set("test", expected)
271 | received, err := ctx.GetFloat32("test")
272 | if err != nil {
273 | t.Error(err)
274 | }
275 | if received != expected {
276 | t.Errorf("expected %+v but received %+v", expected, received)
277 | }
278 | }
279 |
280 | func TestContext_GetFloat32_WithDefaultValue(t *testing.T) {
281 | ctx := NewContext()
282 | defaultValue := float32(123.5)
283 | received, err := ctx.GetFloat32("test", defaultValue)
284 | if err != nil {
285 | t.Error(err)
286 | }
287 | if received != defaultValue {
288 | t.Errorf("expected %+v but received %+v", defaultValue, received)
289 | }
290 | }
291 |
292 | func TestContext_GetFloat32_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
293 | ctx := NewContext()
294 | _, err := ctx.GetFloat32("test", 123.5, 123.5)
295 | if err == nil {
296 | t.Error("the GetFloat32 should return an error")
297 | }
298 | }
299 |
300 | func TestContext_GetFloat32_ErrorOnNotFound(t *testing.T) {
301 | ctx := NewContext()
302 | _, err := ctx.GetFloat32("test")
303 | if err == nil {
304 | t.Error("the GetFloat32 should return an error")
305 | }
306 | }
307 |
308 | func TestContext_GetFloat64(t *testing.T) {
309 | ctx := NewContext()
310 | expected := float64(123.5)
311 | ctx.Set("test", expected)
312 | received, err := ctx.GetFloat64("test")
313 | if err != nil {
314 | t.Error(err)
315 | }
316 | if received != expected {
317 | t.Errorf("expected %+v but received %+v", expected, received)
318 | }
319 | }
320 |
321 | func TestContext_GetFloat64_WithDefaultValue(t *testing.T) {
322 | ctx := NewContext()
323 | defaultValue := float64(123.5)
324 | received, err := ctx.GetFloat64("test", defaultValue)
325 | if err != nil {
326 | t.Error(err)
327 | }
328 | if received != defaultValue {
329 | t.Errorf("expected %+v but received %+v", defaultValue, received)
330 | }
331 | }
332 |
333 | func TestContext_GetFloat64_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
334 | ctx := NewContext()
335 | _, err := ctx.GetFloat64("test", 123.5, 123.5)
336 | if err == nil {
337 | t.Error("the GetFloat64 should return an error")
338 | }
339 | }
340 |
341 | func TestContext_GetFloat64_ErrorOnNotFound(t *testing.T) {
342 | ctx := NewContext()
343 | _, err := ctx.GetFloat64("test")
344 | if err == nil {
345 | t.Error("the GetFloat64 should return an error")
346 | }
347 | }
348 |
349 | func TestContext_GetBool(t *testing.T) {
350 | ctx := NewContext()
351 | expected := bool(false)
352 | ctx.Set("test", expected)
353 | received, err := ctx.GetBool("test")
354 | if err != nil {
355 | t.Error(err)
356 | }
357 | if received != expected {
358 | t.Errorf("expected %+v but received %+v", expected, received)
359 | }
360 | }
361 |
362 | func TestContext_GetBool_WithDefaultValue(t *testing.T) {
363 | ctx := NewContext()
364 | defaultValue := bool(false)
365 | received, err := ctx.GetBool("test", defaultValue)
366 | if err != nil {
367 | t.Error(err)
368 | }
369 | if received != defaultValue {
370 | t.Errorf("expected %+v but received %+v", defaultValue, received)
371 | }
372 | }
373 |
374 | func TestContext_GetBool_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
375 | ctx := NewContext()
376 | _, err := ctx.GetBool("test", false, false)
377 | if err == nil {
378 | t.Error("the GetBool should return an error")
379 | }
380 | }
381 |
382 | func TestContext_GetBool_ErrorOnNotFound(t *testing.T) {
383 | ctx := NewContext()
384 | _, err := ctx.GetBool("test")
385 | if err == nil {
386 | t.Error("the GetBool should return an error")
387 | }
388 | }
389 |
390 |
--------------------------------------------------------------------------------
/context_test.go:
--------------------------------------------------------------------------------
1 | package gobdd
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/require"
7 | )
8 |
9 | func TestContextNilInGetError(t *testing.T) {
10 | ctx := NewContext()
11 | ctx.Set("err", nil)
12 |
13 | res, err := ctx.GetError("err")
14 | require.NoError(t, err)
15 | require.NoError(t, res)
16 | }
17 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // This project is created as the alternative to https://github.com/DATA-DOG/godog and is inspirited by it.
2 | // There are a few differences between both solutions:
3 | //
4 | // - GoBDD uses the built-in testing framework
5 | //
6 | // - GoBDD is run as standard test case (not a separate program)
7 | //
8 | // - you can use every Go native feature like build tags, pprof and so on
9 | //
10 | // - the context in every test case contains all the required information to run (values passed from previous steps).
11 | // More information can be found in the readme file https://github.com/go-bdd/gobdd/blob/master/README.md
12 | package gobdd
13 |
--------------------------------------------------------------------------------
/docs/404.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
18 |
19 |
20 |
404
21 |
22 |
Page not found :(
23 |
The requested page could not be found.
24 |
25 |
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Hello! This is where you manage which Jekyll version is used to run.
4 | # When you want to use a different version, change it below, save the
5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
6 | #
7 | # bundle exec jekyll serve
8 | #
9 | # This will help ensure the proper Jekyll version is running.
10 | # Happy Jekylling!
11 | gem "jekyll", "~> 3.8.5"
12 |
13 | # This is the default theme for new Jekyll sites. You may change this to anything you like.
14 | gem "jekyll-whiteglass"
15 |
16 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
17 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
18 | # gem "github-pages", group: :jekyll_plugins
19 |
20 | # If you have any plugins, put them here!
21 | group :jekyll_plugins do
22 | gem "jekyll-feed", "~> 0.6"
23 | end
24 |
25 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
26 | gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
27 |
28 | # Performance-booster for watching directories on Windows
29 | gem "wdm", "~> 0.1.0" if Gem.win_platform?
30 |
--------------------------------------------------------------------------------
/docs/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | addressable (2.8.0)
5 | public_suffix (>= 2.0.2, < 5.0)
6 | colorator (1.1.0)
7 | concurrent-ruby (1.1.5)
8 | em-websocket (0.5.1)
9 | eventmachine (>= 0.12.9)
10 | http_parser.rb (~> 0.6.0)
11 | eventmachine (1.2.7)
12 | ffi (1.11.1)
13 | forwardable-extended (2.6.0)
14 | http_parser.rb (0.6.0)
15 | i18n (0.9.5)
16 | concurrent-ruby (~> 1.0)
17 | jekyll (3.8.6)
18 | addressable (~> 2.4)
19 | colorator (~> 1.0)
20 | em-websocket (~> 0.5)
21 | i18n (~> 0.7)
22 | jekyll-sass-converter (~> 1.0)
23 | jekyll-watch (~> 2.0)
24 | kramdown (~> 1.14)
25 | liquid (~> 4.0)
26 | mercenary (~> 0.3.3)
27 | pathutil (~> 0.9)
28 | rouge (>= 1.7, < 4)
29 | safe_yaml (~> 1.0)
30 | jekyll-archives (2.2.1)
31 | jekyll (>= 3.6, < 5.0)
32 | jekyll-feed (0.12.1)
33 | jekyll (>= 3.7, < 5.0)
34 | jekyll-paginate (1.1.0)
35 | jekyll-sass-converter (1.5.2)
36 | sass (~> 3.4)
37 | jekyll-sitemap (1.3.1)
38 | jekyll (>= 3.7, < 5.0)
39 | jekyll-watch (2.2.1)
40 | listen (~> 3.0)
41 | jekyll-whiteglass (1.9.1)
42 | jekyll (>= 3.3)
43 | jekyll-archives (~> 2.1)
44 | jekyll-paginate (~> 1.1)
45 | jekyll-sitemap (~> 1.0)
46 | kramdown (1.17.0)
47 | liquid (4.0.3)
48 | listen (3.1.5)
49 | rb-fsevent (~> 0.9, >= 0.9.4)
50 | rb-inotify (~> 0.9, >= 0.9.7)
51 | ruby_dep (~> 1.2)
52 | mercenary (0.3.6)
53 | pathutil (0.16.2)
54 | forwardable-extended (~> 2.6)
55 | public_suffix (4.0.6)
56 | rb-fsevent (0.10.3)
57 | rb-inotify (0.10.0)
58 | ffi (~> 1.0)
59 | rouge (3.11.0)
60 | ruby_dep (1.5.0)
61 | safe_yaml (1.0.5)
62 | sass (3.7.4)
63 | sass-listen (~> 4.0.0)
64 | sass-listen (4.0.0)
65 | rb-fsevent (~> 0.9, >= 0.9.4)
66 | rb-inotify (~> 0.9, >= 0.9.7)
67 |
68 | PLATFORMS
69 | ruby
70 |
71 | DEPENDENCIES
72 | jekyll (~> 3.8.5)
73 | jekyll-feed (~> 0.6)
74 | jekyll-whiteglass
75 | tzinfo-data
76 |
77 | BUNDLED WITH
78 | 2.0.2
79 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | # Welcome to Jekyll!
2 | #
3 | # This config file is meant for settings that affect your whole blog, values
4 | # which you are expected to set up once and rarely edit after that. If you find
5 | # yourself editing this file very often, consider using Jekyll's data files
6 | # feature for the data you need to update frequently.
7 | #
8 | # For technical reasons, this file is *NOT* reloaded automatically when you use
9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process.
10 |
11 | # Site settings
12 | # These are used to personalize your new site. If you look in the HTML files,
13 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
14 | # You can create any custom variable you would like, and they will be accessible
15 | # in the templates via {{ site.myvariable }}.
16 | title: GoBDD
17 | email: your-email@example.com
18 | description: >- # this means to ignore newlines until "baseurl:"
19 | Write an awesome description for your new site here. You can edit this
20 | line in _config.yml. It will appear in your document head meta (for
21 | Google search results) and in your feed.xml site description.
22 | baseurl: "/gobdd" # the subpath of your site, e.g. /blog
23 | url: "" # the base hostname & protocol for your site, e.g. http://example.com
24 | twitter_username: jekyllrb
25 | github_username: jekyll
26 |
27 | # Build settings
28 | markdown: kramdown
29 | remote_theme: yous/whiteglass
30 | theme: jekyll-theme-hacker
31 | plugins:
32 | - jekyll-feed
33 |
34 | # Exclude from processing.
35 | # The following items will not be processed, by default. Create a custom list
36 | # to override the default setting.
37 | # exclude:
38 | # - Gemfile
39 | # - Gemfile.lock
40 | # - node_modules
41 | # - vendor/bundle/
42 | # - vendor/cache/
43 | # - vendor/gems/
44 | # - vendor/ruby/
45 |
--------------------------------------------------------------------------------
/docs/_data/navigation.yml:
--------------------------------------------------------------------------------
1 | main:
2 | - title: "Quick start"
3 | url: /quick-start.html
4 | - title: "Context"
5 | url: /context.html
6 | - title: "Creating steps"
7 | url: /creating-steps.html
8 | - title: "Suite's options"
9 | url: /suite-options.html
10 | - title: "Parameter types"
11 | url: /parameter-types.html
12 | - title: "GitHub"
13 | url: https://github.com/go-bdd/gobdd
14 |
--------------------------------------------------------------------------------
/docs/archives.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: archive
3 | title: "Blog Archive"
4 | permalink: /archives/
5 | ---
6 |
--------------------------------------------------------------------------------
/docs/context.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Context
4 | ---
5 |
6 | # Context
7 |
8 | The context contains the data (state) from previous steps.
9 |
10 | #### Passing data between steps
11 |
12 | The context holds all the data from previously executed steps. They are accessible by `Context.GetX(key interface{})` functions:
13 |
14 | * `Context.GetInt(key interface{}) (int, error)`
15 | * `Context.GetFloat32(key interface{}) (float32, error)`
16 | * `Context.GetFloat64(key interface{}) (float64, error)`
17 | * `Context.GetString(key interface{}) (string, error)`
18 | * and so on...
19 |
20 | When you want to share some data between steps, use the `Context.Set(key, value interface{})` function
21 |
22 | ```go
23 | // in the first step
24 | ctx.Set(name{}, "John")
25 |
26 | // in the second step
27 | val, err := ctx.GetString(name{})
28 | fmt.Printf("Hi %s\n", val) // prints "Hi John"
29 | ```
30 |
31 | When the data is not provided, the whole test will fail.
32 |
33 | #### Predefined keys
34 |
35 | The context holds current test state `testing.T`. It is accessible by calling `Context.Get(TestingTKey{})`. This is useful if you need access to the test state from scenario or step hooks.
36 |
37 | It is also possible to access references to current feature and scenario by calling `Context.Get(FeatureNameKey{})` and `Context.Get(ScenarioNameKey{})` respectively.
38 |
39 | ```go
40 | value, err := ctx.Get(FeatureKey{})
41 | feature, ok := value.(*msgs.GherkinDocument_Feature)
42 | ```
43 |
44 | ```go
45 | value, err := ctx.Get(ScenarioKey{})
46 | scenario, ok := value.(*msgs.GherkinDocument_Feature_Scenario)
47 | ```
48 |
49 | ## Good practices
50 |
51 | It's a good practice to use custom structs as keys instead of strings or any built-in types to avoid collisions between steps using context.
52 |
--------------------------------------------------------------------------------
/docs/creating-steps.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Creating steps
4 | ---
5 |
6 | # Creating steps
7 |
8 | Every step function should accept the `StepTest` as the first 2 parameters and returns the `Context`. Here's an example:
9 |
10 | ```go
11 | type StepFunc func(gobdd.StepTest, ctx gobdd.Context, var1 int, var2 string)
12 | ```
13 |
14 | What's important to stress - the context is a [custom struct](https://github.com/go-bdd/gobdd/tree/master/context), not the built-in interface.
15 | To retrieve information from previously executed you should use functions `ctx.Get*(0)`. Replace the `*` with the type you need. Examples:
16 |
17 | * `ctx.GetInt(type1{})`
18 | * `ctx.GetString(type2{})`
19 | * `ctx.Get(type3{})` - returns raw `[]byte`
20 |
21 | If the value does not exist the test will fail.
22 |
23 | There's possibility to pass a default value as well.
24 |
25 | ```go
26 | ctx.GetFloat32(myFloatValue{}, 123)
27 | ```
28 |
29 | If the `myFloatValue{}` value doesn't exists the `123` will be returned.
30 |
31 | ## Hooks
32 |
33 | There's a possibility to define hooks which might be helpful building useful reporting, visualization, etc.
34 |
35 | * `WithBeforeStep(f func(ctx Context))` configures functions that should be executed before every step
36 | * `WithAfterStep(f func(ctx Context))` configures functions that should be executed after every step
37 |
38 | ```go
39 | suite := NewSuite(
40 | t,
41 | WithBeforeStep(func(ctx Context) {
42 | ctx.Set(time.Time{}, time.Now())
43 | }),
44 | WithAfterStep(func(ctx Context) {
45 | start, _ := ctx.Get(time.Time{})
46 | log.Printf("step took %s", time.Since(start.(time.Time)))
47 | }),
48 | )
49 | ```
50 |
51 | ## Good practices
52 |
53 | Steps should be immutable and only communicate through [the context]({{ site.baseurl }}/context.html).
54 |
--------------------------------------------------------------------------------
/docs/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | jekyll:
5 | image: jekyll/jekyll:latest
6 | command: jekyll serve --watch --force_polling --verbose
7 | ports:
8 | - 4000:4000
9 | volumes:
10 | - .:/srv/jekyll
11 |
--------------------------------------------------------------------------------
/docs/feed.xml:
--------------------------------------------------------------------------------
1 | ---
2 | layout: feed
3 | ---
4 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 | ---
4 |
5 | ## Why did I make the library?
6 |
7 | There is [godog](https://github.com/DATA-DOG/godog) library for BDD tests in Go. I found this library useful but it run as an external application which compiles our code. It has a several disadvantages:
8 |
9 | * no debugging (breakpoints) in the test. Sometimes it's useful to go through the whole execution step by step
10 | * metrics don't count the test run this way
11 | * some style checkers recognise tests as dead code
12 | * it's impossible to use built-in features like [build constraints](https://golang.org/pkg/go/build/#hdr-Build_Constraints).
13 | * no context in steps - so the state have to be stored somewhere else - in my opinion, it makes the maintenance harder
14 |
15 | ## More details about the implementation
16 |
17 | The features use [gherkin](https://cucumber.io/docs/gherkin/reference/) syntax. It means that every document (which contains features etc) have to be compatalble with it.
18 |
19 | Firstly, the library reads all available documents. By default, `features/*.feature` files. Then, loads all the step definitions. Next, tries to execute every scenario and steps into the scenario one by one. At the end, it produces the report of the execution.
20 |
21 | You can have multiple gherkin documents executed within one test.
22 |
--------------------------------------------------------------------------------
/docs/parameter-types.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Parameter types
4 | ---
5 |
6 | # Parameter types
7 |
8 | GoBDD has support for [parameter types](https://cucumber.io/docs/cucumber/cucumber-expressions/). There are a few predefined parameter types:
9 |
10 | * `{int}` - integer (-1 or 56)
11 | * `{float}` - float (0.4 or 234.4)
12 | * `{word}` - single word (`hello` or `pizza`)
13 | * `{text}` - single-quoted or double-quoted strings (`'I like pizza!'` or `"I like pizza!"`)
14 |
15 | You can add your own parameter types using `AddParameterTypes()` function. Here are a few examples
16 |
17 | ```go
18 | s := gobdd.NewSuite(t)
19 | s.AddParameterTypes(`{int}`, []string{`(-?\d+)`})
20 | s.AddParameterTypes(`{float}`, []string{`([-+]?\d*\.?\d+)`})
21 | s.AddParameterTypes(`{word}`, []string{`([^\s]+)`})
22 | s.AddParameterTypes(`{text}`, []string{`"([^"\\]*(?:\\.[^"\\]*)*)"`, `'([^'\\]*(?:\\.[^'\\]*)*)'`})
23 | ```
24 |
25 | The first argument accepts the parameter types. As the second parameter provides list of regular expressions that should replace the parameter.
26 |
27 | Parameter types should be added Before adding any step.
--------------------------------------------------------------------------------
/docs/quick-start.markdown:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Quick start
4 | ---
5 |
6 | Add the package to your project:
7 |
8 | ```
9 | go get github.com/go-bdd/gobdd
10 | ```
11 |
12 | Add a new test `main_test.go`:
13 |
14 | ```go
15 | func add(t gobdd.StepTest, ctx gobdd.Context, var1, var2 int) {
16 | res := var1 + var2
17 | ctx.Set("sumRes", res)
18 | }
19 |
20 | func check(t gobdd.StepTest, ctx gobdd.Context, sum int) {
21 | received, err := ctx.GetInt("sumRes")
22 | if err != nil {
23 | t.Error(err)
24 |
25 | return
26 | }
27 |
28 | if sum != received {
29 | t.Error(errors.New("the math does not work for you"))
30 | }
31 | }
32 |
33 | func TestScenarios(t *testing.T) {
34 | suite := NewSuite(t)
35 | suite.AddStep(`I add (\d+) and (\d+)`, add)
36 | suite.AddStep(`the result should equal (\d+)`, check)
37 | suite.Run()
38 | }
39 | ```
40 |
41 | Inside `features` folder create your scenarios. Here is an example:
42 |
43 | ```gherkin
44 | Feature: math operations
45 | Scenario: add two digits
46 | When I add 1 and 2
47 | Then the result should equal 3
48 | ```
49 |
50 | and run tests
51 |
52 | ```bash
53 | go test ./...
54 | ```
55 |
--------------------------------------------------------------------------------
/docs/suite-options.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | title: Suite's options
4 | ---
5 |
6 | # Suite's options
7 |
8 | The suite can be confiugred using one of these functions:
9 |
10 | * `RunInParallel()` - enables running steps in parallel. It uses the stanard `T.Parallel` function.
11 | * `WithFeaturesPath(path string)` - configures the path where GoBDD should look for features. The default value is `features/*.feature`.
12 | * `WithFeaturesFS(fs fs.FS, path string)` - configures the filesystem and a path (glob pattern) where GoBDD should look for features.
13 | * `WithTags(tags ...string)` - configures which tags should be run. Every tag has to start with `@`.
14 | * `WithBeforeScenario(f func())` - this function `f` will be called before every scenario.
15 | * `WithAfterScenario(f func())` - this funcion `f` will be called after every scenario.
16 | * `WithIgnoredTags(tags ...string)` - configures tags which should be ignored and excluded from execution.
17 |
18 | ## Usage
19 |
20 | Here are some examples of the usage of those functions:
21 |
22 | ```go
23 | suite := NewSuite(t, WithFeaturesPath("features/func_types.feature"))
24 | ```
25 |
26 | ```go
27 | suite := NewSuite(t, WithFeaturesPath("features/tags.feature"), WithTags([]string{"@tag"}))
28 | ```
29 |
30 | As of Go 1.16 you can embed feature files into the test binary and use `fs.FS` as a feature source:
31 |
32 | ```go
33 | import (
34 | "embed"
35 | )
36 |
37 | //go:embed features/*.feature
38 | var featuresFS embed.FS
39 |
40 | // ...
41 |
42 | suite := NewSuite(t, WithFeaturesFS(featuresFS, "*.feature"))
43 | ```
44 |
45 | While in most cases it doesn't make any difference, embedding feature files makes your tests more portable.
46 |
--------------------------------------------------------------------------------
/features.go:
--------------------------------------------------------------------------------
1 | //go:build go1.16
2 | // +build go1.16
3 |
4 | package gobdd
5 |
6 | import (
7 | "embed"
8 | )
9 |
10 | //go:embed features/*.feature
11 | var featuresFS embed.FS
12 |
--------------------------------------------------------------------------------
/features/argument.feature:
--------------------------------------------------------------------------------
1 | Feature: Argument feature
2 | Scenario: compare text with argument
3 | When I concat text "Hello " and argument:
4 | """
5 | World!
6 | """
7 | Then the result should equal argument:
8 | """
9 | Hello World!
10 | """
11 | Scenario: compare text with multiline argument
12 | When I concat text "Hello " and argument:
13 | """
14 | New
15 | World!
16 | """
17 | Then the result should equal argument:
18 | """
19 | Hello New
20 | World!
21 | """
22 |
--------------------------------------------------------------------------------
/features/background.feature:
--------------------------------------------------------------------------------
1 | Feature: using background steps
2 | Background: adding
3 | When I add 1 and 2
4 |
5 | Scenario: the background step should be executed
6 | Then the result should equal 3
7 |
8 | Rule: adding and concat
9 | Background: concat
10 | When I concat word Hello and text " World!"
11 |
12 | Scenario: the background steps should be executed
13 | Then the result should equal 3
14 | Then the result should equal text "Hello World!"
15 |
--------------------------------------------------------------------------------
/features/datatable.feature:
--------------------------------------------------------------------------------
1 | Feature: dataTable feature
2 | Scenario: compare text with dataTable
3 | When I concat all the columns and row together using " - " to separate the columns
4 | |r1c1|r1c2|r1c3|
5 | |r2c1|r2c2|r2c3|
6 | |r3c1|r3c2|r3c3|
7 | Then the result should equal argument:
8 | """
9 | r1c1 - r1c2 - r1c3
10 | r2c1 - r2c2 - r2c3
11 | r3c1 - r3c2 - r3c3
12 | """
--------------------------------------------------------------------------------
/features/empty.feature:
--------------------------------------------------------------------------------
1 | Feature: empty
2 | Scenario: empty
--------------------------------------------------------------------------------
/features/example.feature:
--------------------------------------------------------------------------------
1 | Feature: math operations
2 | Scenario: add two digits
3 | When I add 1 and 2
4 | Then the result should equal 3
5 |
6 |
--------------------------------------------------------------------------------
/features/example_rule.feature:
--------------------------------------------------------------------------------
1 | Feature: math operations
2 | Rule: add things
3 | Scenario: add two digits
4 | When I add 1 and 2
5 | Then the result should equal 3
6 |
7 |
--------------------------------------------------------------------------------
/features/filter_tags_negative.feature:
--------------------------------------------------------------------------------
1 | Feature: do not run this
2 | Scenario: the scenario should not executed
3 | Then fail the test
--------------------------------------------------------------------------------
/features/filter_tags_positive.feature:
--------------------------------------------------------------------------------
1 | @run-this
2 | Feature: run this
3 | Scenario: the scenario should executed
4 | Then the test should pass
--------------------------------------------------------------------------------
/features/func_types.feature:
--------------------------------------------------------------------------------
1 | Feature: math operations in floats
2 | Scenario: add two floats
3 | When I add 1.0 and 2.3
4 | Then the result should equal 3.3
--------------------------------------------------------------------------------
/features/http.feature:
--------------------------------------------------------------------------------
1 | Feature: HTTP requests
2 | Scenario: test GET request
3 | When I make a GET request to "/health"
4 | Then the response code equals 200
5 | Scenario: not existing URI
6 | When I make a GET request to "/not-exists"
7 | Then the response code equals 404
8 | Scenario: testing JSON validation
9 | When I make a GET request to "/json"
10 | Then the response contains a valid JSON
11 | And the response is "{"valid": "json"}"
12 |
13 | Scenario: the simplest request
14 | When I have a GET request "http://google.com"
15 | Then the request has method set to GET
16 | And the url is set to "http://google.com"
17 | And the request body is nil
18 | Scenario: passing headers
19 | Given I have a GET request "http://google.com"
20 | When I set the header "XYZ" to "ZYX"
21 | Then the request has header "XYZ" set to "ZYX"
22 |
23 | Scenario: handling response headers
24 | Given I have a GET request "/mirror"
25 | And I set request header "Xyz" to "ZZZ"
26 | When I make the request
27 | Then the response header "Xyz" equals "ZZZ"
28 |
29 | Scenario: request should have empty body by default
30 | Given I have a POST request "/mirror"
31 | When I set request body to "LALA"
32 | Then the request has body "LALA"
--------------------------------------------------------------------------------
/features/ignored_feature_tags.feature:
--------------------------------------------------------------------------------
1 | @ignore
2 | Feature: ignored tags
3 | Scenario: the scenario should be ignored
4 | Then fail the test
5 |
--------------------------------------------------------------------------------
/features/ignored_rule_tags.feature:
--------------------------------------------------------------------------------
1 |
2 | Feature: ignored tags
3 | @ignore
4 | Rule: this rule should be ignored
5 | Scenario: the scenario should be ignored
6 | Then fail the test
7 | Rule: this rule should run
8 | Scenario: the scenario should pass
9 | Then the test should pass
10 |
--------------------------------------------------------------------------------
/features/ignored_tags.feature:
--------------------------------------------------------------------------------
1 | Feature: ignored tags
2 | @ignore
3 | Scenario: the scenario should be ignored
4 | Then fail the test
5 | Scenario: the scenario should pass
6 | Then the test should pass
7 |
--------------------------------------------------------------------------------
/features/outline.feature:
--------------------------------------------------------------------------------
1 | Feature: Scenario Outline
2 | Scenario Outline: testing outline scenarios
3 | When I add and
4 | Then the result should equal
5 | Examples:
6 | | digit1 | digit2 | result |
7 | | 1 | 2 | 3 |
8 | | 5 | 5 | 10 |
--------------------------------------------------------------------------------
/features/parameter-types.feature:
--------------------------------------------------------------------------------
1 | Feature: parameter types
2 | Scenario: add two digits
3 | When I add 1 and 2
4 | Then the result should equal 3
5 | Scenario: simple word
6 | When I use word pizza
7 | Scenario: simple text with double quotes
8 | When I use text "I like pizza!"
9 | Then the result should equal text 'I like pizza!'
10 | Scenario: simple text with single quotes
11 | When I use text 'I like pizza!'
12 | Then the result should equal text 'I like pizza!'
13 | Scenario: add two floats
14 | When I add floats -1.2 and 2.4
15 | Then the result should equal float 1.2
16 | Scenario: concat a word and a text with single quotes
17 | When I concat word Hello and text ' World!'
18 | Then the result should equal text 'Hello World!'
19 | Scenario: concat a word and a text with double quotes
20 | When I concat word Hello and text " World!"
21 | Then the result should equal text "Hello World!"
22 | Scenario: format text
23 | When I format text "counter %d" with int -12
24 | Then the result should equal text "counter -12"
25 |
--------------------------------------------------------------------------------
/features/tags.feature:
--------------------------------------------------------------------------------
1 | Feature: ignored tags
2 | @tag
3 | Scenario: the scenario should be pass
4 | Then the test should pass
5 | @tag
6 | Scenario Outline: the scenario should be pass
7 | Then the test should pass
8 | Examples:
9 | Scenario: the test should never be executed
10 | Then fail the test
11 |
12 | Rule: the rule should never be executed
13 | Scenario: the test in ignored rule should never be executed
14 | Then fail the test
15 |
16 | Rule: this rule should run
17 | @tag
18 | Scenario: the test in executed rule should pass
19 | Then the test should pass
20 | Scenario: the test in executed rule should never be executed
21 | Then fail the test
--------------------------------------------------------------------------------
/gen.go:
--------------------------------------------------------------------------------
1 | //go:generate go run gen/ctxgen.go
2 |
3 | package gobdd
4 |
--------------------------------------------------------------------------------
/gen/ctxgen.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "html/template"
5 | "log"
6 | "os"
7 | "strings"
8 | )
9 |
10 | type typeDef struct {
11 | Name string
12 | Value string
13 | Zero string
14 | }
15 |
16 | func noescape(str string) template.HTML {
17 | return template.HTML(str)
18 | }
19 |
20 | func main() {
21 | funcMap := template.FuncMap{
22 | "Title": strings.Title,
23 | "noescape": noescape,
24 | }
25 |
26 | types := []typeDef{
27 | {
28 | Name: "string",
29 | Value: `"example text"`,
30 | Zero: `""`,
31 | },
32 | {
33 | Name: "int",
34 | Value: "123",
35 | Zero: "0",
36 | },
37 | {
38 | Name: "int8",
39 | Value: "123",
40 | Zero: "0",
41 | },
42 | {
43 | Name: "int16",
44 | Value: "123",
45 | Zero: "0",
46 | },
47 | {
48 | Name: "int32",
49 | Value: "123",
50 | Zero: "0",
51 | },
52 | {
53 | Name: "int64",
54 | Value: "123",
55 | Zero: "0",
56 | },
57 | {
58 | Name: "float32",
59 | Value: "123.5",
60 | Zero: "0",
61 | },
62 | {
63 | Name: "float64",
64 | Value: "123.5",
65 | Zero: "0",
66 | },
67 | {
68 | Name: "bool",
69 | Value: "false",
70 | Zero: "false",
71 | },
72 | }
73 |
74 | f, err := os.Create("context_get.go")
75 | die(err)
76 |
77 | var tmpl = template.Must(template.New("").Funcs(funcMap).Parse(getTmpl))
78 |
79 | err = tmpl.Execute(f, struct {
80 | Types []typeDef
81 | }{Types: types})
82 | die(err)
83 | f.Close()
84 |
85 | f, err = os.Create("context_get_test.go")
86 | die(err)
87 |
88 | tmpl = template.Must(template.New("").Funcs(funcMap).Parse(testTmpl))
89 |
90 | err = tmpl.Execute(f, struct {
91 | Types []typeDef
92 | }{Types: types})
93 | die(err)
94 | f.Close()
95 | }
96 |
97 | func die(err error) {
98 | if err != nil {
99 | log.Fatal(err)
100 | }
101 | }
102 |
103 | const testTmpl = `// Code generated .* DO NOT EDIT.
104 | package gobdd
105 |
106 | import "testing"
107 | import "errors"
108 |
109 | func TestContext_GetError(t *testing.T) {
110 | ctx := NewContext()
111 | expected := errors.New("new err")
112 | ctx.Set("test", expected)
113 | received, err := ctx.GetError("test")
114 | if err != nil {
115 | t.Error(err)
116 | }
117 | if received != expected {
118 | t.Errorf("expected %+v but received %+v", expected, received)
119 | }
120 | }
121 |
122 | {{ range .Types }}
123 | func TestContext_Get{{ .Name | Title }}(t *testing.T) {
124 | ctx := NewContext()
125 | expected := {{ .Name }}({{ .Value | noescape }})
126 | ctx.Set("test", expected)
127 | received, err := ctx.Get{{ .Name | Title }}("test")
128 | if err != nil {
129 | t.Error(err)
130 | }
131 | if received != expected {
132 | t.Errorf("expected %+v but received %+v", expected, received)
133 | }
134 | }
135 |
136 | func TestContext_Get{{ .Name | Title }}_WithDefaultValue(t *testing.T) {
137 | ctx := NewContext()
138 | defaultValue := {{ .Name }}({{ .Value | noescape }})
139 | received, err := ctx.Get{{ .Name | Title }}("test", defaultValue)
140 | if err != nil {
141 | t.Error(err)
142 | }
143 | if received != defaultValue {
144 | t.Errorf("expected %+v but received %+v", defaultValue, received)
145 | }
146 | }
147 |
148 | func TestContext_Get{{ .Name | Title }}_ShouldReturnErrorWhenMoreThanOneDefaultValue(t *testing.T) {
149 | ctx := NewContext()
150 | _, err := ctx.Get{{ .Name | Title }}("test", {{ .Value | noescape }}, {{ .Value | noescape }})
151 | if err == nil {
152 | t.Error("the Get{{ .Name | Title }} should return an error")
153 | }
154 | }
155 |
156 | func TestContext_Get{{ .Name | Title }}_ErrorOnNotFound(t *testing.T) {
157 | ctx := NewContext()
158 | _, err := ctx.Get{{ .Name | Title }}("test")
159 | if err == nil {
160 | t.Error("the Get{{ .Name | Title }} should return an error")
161 | }
162 | }
163 | {{ end }}
164 | `
165 |
166 | const getTmpl = `// Code generated .* DO NOT EDIT.
167 | package gobdd
168 |
169 | import "fmt"
170 |
171 | {{ range .Types }}
172 | func (ctx Context) Get{{ .Name | Title }}(key interface{}, defaultValue ...{{ .Name }}) ({{ .Name }}, error) {
173 | if len(defaultValue) > 1 {
174 | return {{.Zero|noescape}}, fmt.Errorf("allowed to pass only 1 default value but %d got", len(defaultValue))
175 | }
176 |
177 | if _, ok := ctx.values[key]; !ok {
178 | if len(defaultValue) == 1 {
179 | return defaultValue[0], nil
180 | }
181 | return {{.Zero|noescape}}, fmt.Errorf("the key %+v does not exist", key)
182 | }
183 |
184 | value, ok := ctx.values[key].({{ .Name }})
185 | if !ok {
186 | return {{.Zero|noescape}}, fmt.Errorf("the expected value is not {{ .Name }} (%T)", key)
187 | }
188 | return value, nil
189 | }
190 | {{ end }}
191 | `
192 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/go-bdd/gobdd
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/cucumber/gherkin/go/v28 v28.0.0
7 | github.com/cucumber/messages/go/v24 v24.1.0
8 | github.com/go-bdd/assert v0.0.0-20200713105154-236f01430281
9 | github.com/stretchr/testify v1.9.0
10 | )
11 |
12 | require (
13 | github.com/davecgh/go-spew v1.1.1 // indirect
14 | github.com/gofrs/uuid v4.4.0+incompatible // indirect
15 | github.com/kr/text v0.2.0 // indirect
16 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
17 | github.com/pmezard/go-difflib v1.0.0 // indirect
18 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
19 | gopkg.in/yaml.v3 v3.0.1 // indirect
20 | )
21 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2 | github.com/cucumber/gherkin/go/v28 v28.0.0 h1:SBqwscPOhe83JF0ukpEj+4QZ2ScOpPQByC0gD3cXBkg=
3 | github.com/cucumber/gherkin/go/v28 v28.0.0/go.mod h1:HVwDrzWvtsVbkxHw6KVZFA79x5uSLb+ajzS0BXuHiE8=
4 | github.com/cucumber/messages/go/v24 v24.0.1/go.mod h1:ns4Befq4c4n9/B5APpTlBu5kXL1DVE4+5bbe0vSV4fc=
5 | github.com/cucumber/messages/go/v24 v24.1.0 h1:JMpspvV3IoGwcbEUbsKuxr0tRkFP7aqcQ5SvoanS/DA=
6 | github.com/cucumber/messages/go/v24 v24.1.0/go.mod h1:PR0+ygYqqyT1/C4EmioDdvgR8YG9+SZEkxEKw2jfd8g=
7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/go-bdd/assert v0.0.0-20200713105154-236f01430281 h1:jfonqldVSNVekjmLvHqelFzBba7SwylNIZ3fbM/lKVs=
11 | github.com/go-bdd/assert v0.0.0-20200713105154-236f01430281/go.mod h1:dOoqt7g2I/fpR7/Pyz0P19J3xjDj5lsHn3v9EaFLRjM=
12 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
13 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
15 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
16 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
17 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
18 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
19 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
20 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
23 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
24 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
25 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
26 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
27 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
28 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
29 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
30 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
32 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
33 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
34 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
36 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
37 |
--------------------------------------------------------------------------------
/gobdd.go:
--------------------------------------------------------------------------------
1 | package gobdd
2 |
3 | import (
4 | "bufio"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "os"
9 | "path/filepath"
10 | "reflect"
11 | "regexp"
12 | "strconv"
13 | "strings"
14 | "testing"
15 |
16 | gherkin "github.com/cucumber/gherkin/go/v28"
17 | msgs "github.com/cucumber/messages/go/v24"
18 | )
19 |
20 | const contextArgumentsNumber = 2
21 |
22 | // Suite holds all the information about the suite (options, steps to execute etc)
23 | type Suite struct {
24 | t TestingT
25 | steps []stepDef
26 | options SuiteOptions
27 | hasStepErrors bool
28 | parameterTypes map[string][]string
29 | }
30 |
31 | // SuiteOptions holds all the information about how the suite or features/steps should be configured
32 | type SuiteOptions struct {
33 | featureSource featureSource
34 | ignoreTags []string
35 | tags []string
36 | beforeScenario []func(ctx Context)
37 | afterScenario []func(ctx Context)
38 | beforeStep []func(ctx Context)
39 | afterStep []func(ctx Context)
40 | runInParallel bool
41 | }
42 |
43 | type featureSource interface {
44 | loadFeatures() ([]feature, error)
45 | }
46 |
47 | type feature interface {
48 | Open() (io.Reader, error)
49 | }
50 |
51 | type pathFeatureSource string
52 |
53 | func (s pathFeatureSource) loadFeatures() ([]feature, error) {
54 | files, err := filepath.Glob(string(s))
55 | if err != nil {
56 | return nil, errors.New("cannot find features/ directory")
57 | }
58 |
59 | features := make([]feature, 0, len(files))
60 |
61 | for _, f := range files {
62 | features = append(features, fileFeature(f))
63 | }
64 |
65 | return features, nil
66 | }
67 |
68 | type fileFeature string
69 |
70 | func (f fileFeature) Open() (io.Reader, error) {
71 | file, err := os.Open(string(f))
72 | if err != nil {
73 | return nil, fmt.Errorf("cannot open file %s", f)
74 | }
75 |
76 | return file, nil
77 | }
78 |
79 | // NewSuiteOptions creates a new suite configuration with default values
80 | func NewSuiteOptions() SuiteOptions {
81 | return SuiteOptions{
82 | featureSource: pathFeatureSource("features/*.feature"),
83 | ignoreTags: []string{},
84 | tags: []string{},
85 | beforeScenario: []func(ctx Context){},
86 | afterScenario: []func(ctx Context){},
87 | beforeStep: []func(ctx Context){},
88 | afterStep: []func(ctx Context){},
89 | }
90 | }
91 |
92 | // RunInParallel runs tests in parallel
93 | func RunInParallel() func(*SuiteOptions) {
94 | return func(options *SuiteOptions) {
95 | options.runInParallel = true
96 | }
97 | }
98 |
99 | // WithFeaturesPath configures a pattern (regexp) where feature can be found.
100 | // The default value is "features/*.feature"
101 | func WithFeaturesPath(path string) func(*SuiteOptions) {
102 | return func(options *SuiteOptions) {
103 | options.featureSource = pathFeatureSource(path)
104 | }
105 | }
106 |
107 | // WithTags configures which tags should be skipped while executing a suite
108 | // Every tag has to start with @
109 | func WithTags(tags ...string) func(*SuiteOptions) {
110 | return func(options *SuiteOptions) {
111 | options.tags = tags
112 | }
113 | }
114 |
115 | // WithBeforeScenario configures functions that should be executed before every scenario
116 | func WithBeforeScenario(f func(ctx Context)) func(*SuiteOptions) {
117 | return func(options *SuiteOptions) {
118 | options.beforeScenario = append(options.beforeScenario, f)
119 | }
120 | }
121 |
122 | // WithAfterScenario configures functions that should be executed after every scenario
123 | func WithAfterScenario(f func(ctx Context)) func(*SuiteOptions) {
124 | return func(options *SuiteOptions) {
125 | options.afterScenario = append(options.afterScenario, f)
126 | }
127 | }
128 |
129 | // WithBeforeStep configures functions that should be executed before every step
130 | func WithBeforeStep(f func(ctx Context)) func(*SuiteOptions) {
131 | return func(options *SuiteOptions) {
132 | options.beforeStep = append(options.beforeStep, f)
133 | }
134 | }
135 |
136 | // WithAfterStep configures functions that should be executed after every step
137 | func WithAfterStep(f func(ctx Context)) func(*SuiteOptions) {
138 | return func(options *SuiteOptions) {
139 | options.afterStep = append(options.afterStep, f)
140 | }
141 | }
142 |
143 | // WithIgnoredTags configures which tags should be skipped while executing a suite
144 | // Every tag has to start with @ otherwise will be ignored
145 | func WithIgnoredTags(tags ...string) func(*SuiteOptions) {
146 | return func(options *SuiteOptions) {
147 | options.ignoreTags = tags
148 | }
149 | }
150 |
151 | type stepDef struct {
152 | expr *regexp.Regexp
153 | f interface{}
154 | }
155 |
156 | type StepTest interface {
157 | testing.TB
158 | }
159 |
160 | type TestingT interface {
161 | StepTest
162 | Parallel()
163 | Run(name string, f func(t *testing.T)) bool
164 | }
165 |
166 | // TestingTKey is used to store reference to current *testing.T instance
167 | type TestingTKey struct{}
168 |
169 | // FeatureKey is used to store reference to current *msgs.Feature instance
170 | type FeatureKey struct{}
171 |
172 | // RuleKey is used to store reference to current *msgs.Rule instance
173 | type RuleKey struct{}
174 |
175 | // ScenarioKey is used to store reference to current *msgs.Scenario instance
176 | type ScenarioKey struct{}
177 |
178 | // Creates a new suites with given configuration and empty steps defined
179 | func NewSuite(t TestingT, optionClosures ...func(*SuiteOptions)) *Suite {
180 | options := NewSuiteOptions()
181 |
182 | for i := 0; i < len(optionClosures); i++ {
183 | optionClosures[i](&options)
184 | }
185 |
186 | s := &Suite{
187 | t: t,
188 | steps: []stepDef{},
189 | options: options,
190 | parameterTypes: map[string][]string{},
191 | }
192 |
193 | // see https://github.com/cucumber/cucumber-expressions/blob/main/go/parameter_type_registry.go
194 | s.AddParameterTypes(`{int}`, []string{`(-?\d+)`})
195 | s.AddParameterTypes(`{float}`, []string{`([-+]?\d*\.?\d+)`})
196 | s.AddParameterTypes(`{word}`, []string{`([^\s]+)`})
197 | s.AddParameterTypes(`{text}`, []string{`"([^"\\]*(?:\\.[^"\\]*)*)"`, `'([^'\\]*(?:\\.[^'\\]*)*)'`})
198 |
199 | return s
200 | }
201 |
202 | // AddParameterTypes adds a list of parameter types that will be used to simplify step definitions.
203 | //
204 | // The first argument is the parameter type and the second parameter is a list of regular expressions
205 | // that should replace the parameter type.
206 | //
207 | // s.AddParameterTypes(`{int}`, []string{`(\d)`})
208 | //
209 | // The regular expression should compile, otherwise will produce an error and stop executing.
210 | func (s *Suite) AddParameterTypes(from string, to []string) {
211 | for _, to := range to {
212 | _, err := regexp.Compile(to)
213 | if err != nil {
214 | s.t.Fatalf(`the regular expression for key %s doesn't compile: %s`, from, to)
215 | }
216 |
217 | s.parameterTypes[from] = append(s.parameterTypes[from], to)
218 | }
219 | }
220 |
221 | // AddStep registers a step in the suite.
222 | //
223 | // The second parameter is the step function that gets executed
224 | // when a step definition matches the provided regular expression.
225 | //
226 | // A step function can have any number of parameters (even zero),
227 | // but it MUST accept a gobdd.StepTest and gobdd.Context as the first parameters (if there is any):
228 | //
229 | // func myStepFunction(t gobdd.StepTest, ctx gobdd.Context, first int, second int) {
230 | // }
231 | func (s *Suite) AddStep(expr string, step interface{}) {
232 | err := validateStepFunc(step)
233 | if err != nil {
234 | s.t.Errorf("the step function for step `%s` is incorrect: %s", expr, err.Error())
235 | s.hasStepErrors = true
236 |
237 | return
238 | }
239 |
240 | exprs := s.applyParameterTypes(expr)
241 |
242 | for _, expr := range exprs {
243 | compiled, err := regexp.Compile(expr)
244 | if err != nil {
245 | s.t.Errorf("the step function is incorrect: %s", err.Error())
246 | s.hasStepErrors = true
247 |
248 | return
249 | }
250 |
251 | s.steps = append(s.steps, stepDef{
252 | expr: compiled,
253 | f: step,
254 | })
255 | }
256 | }
257 |
258 | func (s *Suite) applyParameterTypes(expr string) []string {
259 | exprs := []string{expr}
260 |
261 | for from, to := range s.parameterTypes {
262 | for _, t := range to {
263 | if strings.Contains(expr, from) {
264 | exprs = append(exprs, s.applyParameterTypes(strings.ReplaceAll(expr, from, t))...)
265 | }
266 | }
267 | }
268 |
269 | return exprs
270 | }
271 |
272 | // AddRegexStep registers a step in the suite.
273 | //
274 | // The second parameter is the step function that gets executed
275 | // when a step definition matches the provided regular expression.
276 | //
277 | // A step function can have any number of parameters (even zero),
278 | // but it MUST accept a gobdd.StepTest and gobdd.Context as the first parameters (if there is any):
279 | //
280 | // func myStepFunction(t gobdd.StepTest, ctx gobdd.Context, first int, second int) {
281 | // }
282 | func (s *Suite) AddRegexStep(expr *regexp.Regexp, step interface{}) {
283 | err := validateStepFunc(step)
284 | if err != nil {
285 | s.t.Errorf("the step function is incorrect: %s", err.Error())
286 | s.hasStepErrors = true
287 |
288 | return
289 | }
290 |
291 | s.steps = append(s.steps, stepDef{
292 | expr: expr,
293 | f: step,
294 | })
295 | }
296 |
297 | // Executes the suite with given options and defined steps
298 | func (s *Suite) Run() {
299 | if s.hasStepErrors {
300 | s.t.Fatal("the test contains invalid step definitions")
301 |
302 | return
303 | }
304 |
305 | features, err := s.options.featureSource.loadFeatures()
306 | if err != nil {
307 | s.t.Fatal(err.Error())
308 | }
309 |
310 | if s.options.runInParallel {
311 | s.t.Parallel()
312 | }
313 |
314 | for _, feature := range features {
315 | err = s.executeFeature(feature)
316 | if err != nil {
317 | s.t.Fail()
318 | }
319 | }
320 | }
321 |
322 | func (s *Suite) executeFeature(feature feature) error {
323 | f, err := feature.Open()
324 | if err != nil {
325 | return err
326 | }
327 |
328 | if closer, ok := f.(io.Closer); ok {
329 | defer closer.Close()
330 | }
331 |
332 | featureIO := bufio.NewReader(f)
333 |
334 | doc, err := gherkin.ParseGherkinDocument(featureIO, (&msgs.Incrementing{}).NewId)
335 | if err != nil {
336 | s.t.Fatalf("error while loading document: %s\n", err)
337 | }
338 |
339 | if doc.Feature == nil {
340 | return nil
341 | }
342 |
343 | return s.runFeature(doc.Feature)
344 | }
345 |
346 | func (s *Suite) runFeature(feature *msgs.Feature) error {
347 | if s.shouldSkipFeatureOrRule(feature.Tags) {
348 | s.t.Logf("the feature (%s) is ignored ", feature.Name)
349 | return nil
350 | }
351 |
352 | hasErrors := false
353 |
354 | s.t.Run(fmt.Sprintf("%s %s", strings.TrimSpace(feature.Keyword), feature.Name), func(t *testing.T) {
355 | backgrounds := []*msgs.Background{}
356 |
357 | for _, child := range feature.Children {
358 | if child.Background != nil {
359 | backgrounds = append(backgrounds, child.Background)
360 | }
361 |
362 | if rule := child.Rule; rule != nil {
363 | s.runRule(feature, rule, backgrounds, t)
364 | }
365 | if scenario := child.Scenario; scenario != nil {
366 | ctx := NewContext()
367 | ctx.Set(FeatureKey{}, feature)
368 | s.runScenario(ctx, scenario, backgrounds, t, feature.Tags)
369 | }
370 | }
371 | })
372 |
373 | if hasErrors {
374 | return errors.New("the feature contains errors")
375 | }
376 |
377 | return nil
378 | }
379 |
380 | func (s *Suite) getOutlineStep(
381 | steps []*msgs.Step,
382 | examples []*msgs.Examples) []*msgs.Step {
383 | stepsList := make([][]*msgs.Step, len(steps))
384 |
385 | for i, outlineStep := range steps {
386 | for _, example := range examples {
387 | stepsList[i] = append(stepsList[i], s.stepsFromExamples(outlineStep, example)...)
388 | }
389 | }
390 |
391 | var newSteps []*msgs.Step
392 |
393 | if len(stepsList) == 0 {
394 | return newSteps
395 | }
396 |
397 | for ei := range examples {
398 | for ci := range examples[ei].TableBody {
399 | for si := range steps {
400 | newSteps = append(newSteps, stepsList[si][ci])
401 | }
402 | }
403 | }
404 |
405 | return newSteps
406 | }
407 |
408 | func (s *Suite) stepsFromExamples(
409 | sourceStep *msgs.Step,
410 | example *msgs.Examples) []*msgs.Step {
411 | steps := []*msgs.Step{}
412 |
413 | placeholdersValues := []string{}
414 |
415 | if example.TableHeader != nil {
416 | placeholders := example.TableHeader.Cells
417 | for _, placeholder := range placeholders {
418 | ph := "<" + placeholder.Value + ">"
419 | placeholdersValues = append(placeholdersValues, ph)
420 | }
421 | }
422 |
423 | text := sourceStep.Text
424 |
425 | for _, row := range example.TableBody {
426 | // iterate over the cells and update the text
427 | stepText, expr := s.stepFromExample(text, row, placeholdersValues)
428 |
429 | // find step definition for the new step
430 | def, err := s.findStepDef(stepText)
431 | if err != nil {
432 | continue
433 | }
434 |
435 | // add the step to the list
436 | s.AddStep(expr, def.f)
437 |
438 | // clone a step
439 | step := &msgs.Step{
440 | Location: sourceStep.Location,
441 | Keyword: sourceStep.Keyword,
442 | Text: stepText,
443 | KeywordType: sourceStep.KeywordType,
444 | DocString: sourceStep.DocString,
445 | DataTable: sourceStep.DataTable,
446 | Id: sourceStep.Id,
447 | }
448 |
449 | steps = append(steps, step)
450 | }
451 |
452 | return steps
453 | }
454 |
455 | func (s *Suite) stepFromExample(
456 | stepName string,
457 | row *msgs.TableRow, placeholders []string) (string, string) {
458 | expr := stepName
459 |
460 | for i, ph := range placeholders {
461 | t := getRegexpForVar(row.Cells[i].Value)
462 | expr = strings.ReplaceAll(expr, ph, t)
463 | stepName = strings.ReplaceAll(stepName, ph, row.Cells[i].Value)
464 | }
465 |
466 | return stepName, expr
467 | }
468 |
469 | func (s *Suite) callBeforeScenarios(ctx Context) {
470 | for _, f := range s.options.beforeScenario {
471 | f(ctx)
472 | }
473 | }
474 |
475 | func (s *Suite) callAfterScenarios(ctx Context) {
476 | for _, f := range s.options.afterScenario {
477 | f(ctx)
478 | }
479 | }
480 |
481 | func (s *Suite) callBeforeSteps(ctx Context) {
482 | for _, f := range s.options.beforeStep {
483 | f(ctx)
484 | }
485 | }
486 |
487 | func (s *Suite) callAfterSteps(ctx Context) {
488 | for _, f := range s.options.afterStep {
489 | f(ctx)
490 | }
491 | }
492 | func (s *Suite) runRule(feature *msgs.Feature, rule *msgs.Rule,
493 | backgrounds []*msgs.Background, t *testing.T) {
494 | ruleTags := feature.Tags
495 | ruleTags = append(ruleTags, rule.Tags...)
496 |
497 | if s.shouldSkipFeatureOrRule(ruleTags) {
498 | s.t.Logf("the rule (%s) is ignored ", feature.Name)
499 | return
500 | }
501 |
502 | ruleBackgrounds := []*msgs.Background{}
503 | ruleBackgrounds = append(ruleBackgrounds, backgrounds...)
504 |
505 | t.Run(fmt.Sprintf("%s %s", strings.TrimSpace(rule.Keyword), rule.Name), func(t *testing.T) {
506 | for _, ruleChild := range rule.Children {
507 | if ruleChild.Background != nil {
508 | ruleBackgrounds = append(ruleBackgrounds, ruleChild.Background)
509 | }
510 | if scenario := ruleChild.Scenario; scenario != nil {
511 | ctx := NewContext()
512 | ctx.Set(FeatureKey{}, feature)
513 | ctx.Set(RuleKey{}, rule)
514 | s.runScenario(ctx, scenario, ruleBackgrounds, t, ruleTags)
515 | }
516 | }
517 | })
518 | }
519 | func (s *Suite) runScenario(ctx Context, scenario *msgs.Scenario,
520 | backgrounds []*msgs.Background, t *testing.T, parentTags []*msgs.Tag) {
521 | if s.shouldSkipScenario(append(parentTags, scenario.Tags...)) {
522 | t.Logf("Skipping scenario %s", scenario.Name)
523 | return
524 | }
525 |
526 | t.Run(fmt.Sprintf("%s %s", strings.TrimSpace(scenario.Keyword), scenario.Name), func(t *testing.T) {
527 | // NOTE consider passing t as argument to scenario hooks
528 | ctx.Set(ScenarioKey{}, scenario)
529 | ctx.Set(TestingTKey{}, t)
530 | defer ctx.Set(TestingTKey{}, nil)
531 |
532 | s.callBeforeScenarios(ctx)
533 | defer s.callAfterScenarios(ctx)
534 |
535 | if len(backgrounds) > 0 {
536 | steps := s.getBackgroundSteps(backgrounds)
537 | s.runSteps(ctx, t, steps)
538 | }
539 | steps := scenario.Steps
540 | if examples := scenario.Examples; len(examples) > 0 {
541 | c := ctx.Clone()
542 | steps = s.getOutlineStep(scenario.Steps, examples)
543 | s.runSteps(c, t, steps)
544 | } else {
545 | c := ctx.Clone()
546 | s.runSteps(c, t, steps)
547 | }
548 | })
549 | }
550 |
551 | func (s *Suite) runSteps(ctx Context, t *testing.T, steps []*msgs.Step) {
552 | for _, step := range steps {
553 | s.runStep(ctx, t, step)
554 | }
555 | }
556 |
557 | func (s *Suite) runStep(ctx Context, t *testing.T, step *msgs.Step) {
558 | defer func() {
559 | if r := recover(); r != nil {
560 | t.Error(r)
561 | }
562 | }()
563 |
564 | def, err := s.findStepDef(step.Text)
565 | if err != nil {
566 | t.Fatalf("cannot find step definition for step: %s%s", step.Keyword, step.Text)
567 | }
568 |
569 | matches := def.expr.FindSubmatch([]byte(step.Text))[1:]
570 | params := make([]interface{}, 0, len(matches)) // defining the slices capacity instead of the length to use append
571 | for _, m := range matches {
572 | params = append(params, m)
573 | }
574 |
575 | if step.DocString != nil {
576 | params = append(params, step.DocString.Content)
577 | }
578 | if step.DataTable != nil {
579 | params = append(params, *step.DataTable)
580 | }
581 |
582 | t.Run(fmt.Sprintf("%s %s", strings.TrimSpace(step.Keyword), step.Text), func(t *testing.T) {
583 | // NOTE consider passing t as argument to step hooks
584 | ctx.Set(TestingTKey{}, t)
585 | defer ctx.Set(TestingTKey{}, nil)
586 |
587 | s.callBeforeSteps(ctx)
588 | defer s.callAfterSteps(ctx)
589 |
590 | def.run(ctx, t, params)
591 | })
592 | }
593 |
594 | func (def *stepDef) run(ctx Context, t TestingT, params []interface{}) { // nolint:interfacer
595 | defer func() {
596 | if r := recover(); r != nil {
597 | t.Errorf("%+v", r)
598 | }
599 | }()
600 |
601 | d := reflect.ValueOf(def.f)
602 | if len(params)+contextArgumentsNumber != d.Type().NumIn() {
603 | t.Fatalf("the step function %s accepts %d arguments but %d received",
604 | d.String(),
605 | d.Type().NumIn(),
606 | len(params)+contextArgumentsNumber)
607 |
608 | return
609 | }
610 |
611 | in := []reflect.Value{reflect.ValueOf(t), reflect.ValueOf(ctx)}
612 |
613 | for i, v := range params {
614 | if len(params) < i+1 {
615 | break
616 | }
617 |
618 | inType := d.Type().In(i + contextArgumentsNumber)
619 |
620 | paramType, err := paramType(v, inType)
621 | if err != nil {
622 | t.Fatal(err)
623 | }
624 |
625 | in = append(in, paramType)
626 | }
627 |
628 | d.Call(in)
629 | }
630 |
631 | func paramType(param interface{}, inType reflect.Type) (reflect.Value, error) {
632 | switch inType.Kind() { // nolint:exhaustive // the linter does not recognize 'default:' to satisfy exhaustiveness
633 | case reflect.String:
634 | s, err := shouldBeString(param)
635 | return reflect.ValueOf(s), err
636 | case reflect.Int:
637 | v, err := shouldBeInt(param)
638 | return reflect.ValueOf(v), err
639 | case reflect.Float32:
640 | v, err := shouldBeFloat(param, 32) // nolint:mnd
641 | return reflect.ValueOf(float32(v)), err
642 | case reflect.Float64:
643 | v, err := shouldBeFloat(param, 64) // nolint:mnd
644 | return reflect.ValueOf(v), err
645 | case reflect.Slice:
646 | // only []byte is supported
647 | if inType != reflect.TypeOf([]byte(nil)) {
648 | return reflect.Value{}, fmt.Errorf("the slice argument type %s is not supported", inType.Kind())
649 | }
650 |
651 | v, err := shouldBeByteSlice(param)
652 |
653 | return reflect.ValueOf(v), err
654 | case reflect.Struct:
655 | // the only struct supported is the one introduced by cucumber
656 | if inType != reflect.TypeOf(msgs.DataTable{}) {
657 | return reflect.Value{}, fmt.Errorf("the struct argument type %s is not supported", inType.Kind())
658 | }
659 |
660 | v, err := shouldBeDataTable(param)
661 |
662 | return reflect.ValueOf(v), err
663 | default:
664 | return reflect.Value{}, fmt.Errorf("the type %s is not supported", inType.Kind())
665 | }
666 | }
667 |
668 | func shouldBeDataTable(input interface{}) (msgs.DataTable, error) {
669 | if v, ok := input.(msgs.DataTable); ok {
670 | return v, nil
671 | }
672 |
673 | return msgs.DataTable{}, fmt.Errorf("cannot convert %v of type %T to messages.DataTable", input, input)
674 | }
675 |
676 | func shouldBeByteSlice(input interface{}) ([]byte, error) {
677 | if v, ok := input.([]byte); ok {
678 | return v, nil
679 | }
680 |
681 | return nil, fmt.Errorf("cannot convert %v of type %T to []byte", input, input)
682 | }
683 |
684 | func shouldBeInt(input interface{}) (int, error) {
685 | s, err := shouldBeString(input)
686 | if err != nil {
687 | return 0, err
688 | }
689 |
690 | return strconv.Atoi(s)
691 | }
692 |
693 | func shouldBeFloat(input interface{}, bitSize int) (float64, error) {
694 | s, err := shouldBeString(input)
695 | if err != nil {
696 | return 0, err
697 | }
698 |
699 | return strconv.ParseFloat(s, bitSize)
700 | }
701 |
702 | func shouldBeString(input interface{}) (string, error) {
703 | switch v := input.(type) {
704 | case string:
705 | return v, nil
706 | case []byte:
707 | return string(v), nil
708 | default:
709 | return "", fmt.Errorf("cannot convert %v of type %T to string", input, input)
710 | }
711 | }
712 |
713 | func (s *Suite) findStepDef(text string) (stepDef, error) {
714 | var sd stepDef
715 |
716 | found := 0
717 |
718 | for _, step := range s.steps {
719 | if !step.expr.MatchString(text) {
720 | continue
721 | }
722 |
723 | if l := len(step.expr.FindAll([]byte(text), -1)); l > found {
724 | found = l
725 | sd = step
726 | }
727 | }
728 |
729 | if reflect.DeepEqual(sd, stepDef{}) {
730 | return sd, errors.New("cannot find step definition")
731 | }
732 |
733 | return sd, nil
734 | }
735 |
736 | func (s *Suite) shouldSkipFeatureOrRule(featureOrRuleTags []*msgs.Tag) bool {
737 | for _, tag := range featureOrRuleTags {
738 | if contains(s.options.ignoreTags, tag.Name) {
739 | return true
740 | }
741 | }
742 |
743 | return false
744 | }
745 |
746 | func (s *Suite) shouldSkipScenario(scenarioTags []*msgs.Tag) bool {
747 | for _, tag := range scenarioTags {
748 | if contains(s.options.ignoreTags, tag.Name) {
749 | return true
750 | }
751 | }
752 |
753 | if len(s.options.tags) == 0 {
754 | return false
755 | }
756 |
757 | for _, tag := range scenarioTags {
758 | if contains(s.options.tags, tag.Name) {
759 | return false
760 | }
761 | }
762 |
763 | return true
764 | }
765 |
766 | func (s *Suite) getBackgroundSteps(backgrounds []*msgs.Background) []*msgs.Step {
767 | result := []*msgs.Step{}
768 | for _, background := range backgrounds {
769 | result = append(result, background.Steps...)
770 | }
771 |
772 | return result
773 | }
774 |
775 | // contains tells whether a contains x.
776 | func contains(a []string, x string) bool {
777 | for _, n := range a {
778 | if x == n {
779 | return true
780 | }
781 | }
782 |
783 | return false
784 | }
785 |
786 | func getRegexpForVar(v interface{}) string {
787 | s := v.(string)
788 |
789 | if _, err := strconv.Atoi(s); err == nil {
790 | return "(\\d+)"
791 | }
792 |
793 | if _, err := strconv.ParseFloat(s, 32); err == nil {
794 | return "([+-]?([0-9]*[.])?[0-9]+)"
795 | }
796 |
797 | return "(.*)"
798 | }
799 |
--------------------------------------------------------------------------------
/gobdd_go1_16.go:
--------------------------------------------------------------------------------
1 | //go:build go1.16
2 | // +build go1.16
3 |
4 | package gobdd
5 |
6 | import (
7 | "fmt"
8 | "io"
9 | "io/fs"
10 | )
11 |
12 | // WithFeaturesFS configures a filesystem and a path (glob pattern) where features can be found.
13 | func WithFeaturesFS(fs fs.FS, path string) func(*SuiteOptions) {
14 | return func(options *SuiteOptions) {
15 | options.featureSource = fsFeatureSource{
16 | fs: fs,
17 | path: path,
18 | }
19 | }
20 | }
21 |
22 | type fsFeatureSource struct {
23 | fs fs.FS
24 | path string
25 | }
26 |
27 | func (s fsFeatureSource) loadFeatures() ([]feature, error) {
28 | files, err := fs.Glob(s.fs, s.path)
29 | if err != nil {
30 | return nil, fmt.Errorf("loading features: %w", err)
31 | }
32 |
33 | features := make([]feature, 0, len(files))
34 |
35 | for _, f := range files {
36 | features = append(features, fsFeature{
37 | fs: s.fs,
38 | file: f,
39 | })
40 | }
41 |
42 | return features, nil
43 | }
44 |
45 | type fsFeature struct {
46 | fs fs.FS
47 | file string
48 | }
49 |
50 | func (f fsFeature) Open() (io.Reader, error) {
51 | file, err := f.fs.Open(f.file)
52 | if err != nil {
53 | return nil, fmt.Errorf("opening feature: %w", err)
54 | }
55 |
56 | return file, nil
57 | }
58 |
--------------------------------------------------------------------------------
/gobdd_go1_16_test.go:
--------------------------------------------------------------------------------
1 | //go:build go1.16
2 | // +build go1.16
3 |
4 | package gobdd
5 |
6 | import (
7 | "regexp"
8 | "testing"
9 | )
10 |
11 | func TestWithFeaturesFS(t *testing.T) {
12 | suite := NewSuite(t, WithFeaturesFS(featuresFS, "example.feature"))
13 | compiled := regexp.MustCompile(`I add (\d+) and (\d+)`)
14 | suite.AddRegexStep(compiled, add)
15 | compiled = regexp.MustCompile(`the result should equal (\d+)`)
16 | suite.AddRegexStep(compiled, check)
17 |
18 | suite.Run()
19 | }
20 |
--------------------------------------------------------------------------------
/gobdd_test.go:
--------------------------------------------------------------------------------
1 | package gobdd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "regexp"
7 | "strings"
8 | "testing"
9 |
10 | msgs "github.com/cucumber/messages/go/v24"
11 | "github.com/go-bdd/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestScenarios(t *testing.T) {
16 | suite := NewSuite(t, WithFeaturesPath("features/example.feature"))
17 | compiled := regexp.MustCompile(`I add (\d+) and (\d+)`)
18 | suite.AddRegexStep(compiled, add)
19 | compiled = regexp.MustCompile(`the result should equal (\d+)`)
20 | suite.AddRegexStep(compiled, check)
21 | suite.Run()
22 | }
23 |
24 | func TestRule(t *testing.T) {
25 | suite := NewSuite(t, WithFeaturesPath("features/example_rule.feature"))
26 | compiled := regexp.MustCompile(`I add (\d+) and (\d+)`)
27 | suite.AddRegexStep(compiled, add)
28 | compiled = regexp.MustCompile(`the result should equal (\d+)`)
29 | suite.AddRegexStep(compiled, check)
30 | suite.Run()
31 | }
32 |
33 | func TestAddStepWithRegexp(t *testing.T) {
34 | suite := NewSuite(t, WithFeaturesPath("features/example.feature"))
35 | suite.AddStep(`I add (\d+) and (\d+)`, add)
36 | suite.AddStep(`the result should equal (\d+)`, check)
37 |
38 | suite.Run()
39 | }
40 |
41 | func TestDifferentFuncTypes(t *testing.T) {
42 | suite := NewSuite(t, WithFeaturesPath("features/func_types.feature"))
43 | suite.AddStep(`I add ([+-]?[0-9]*[.]?[0-9]+) and ([+-]?[0-9]*[.]?[0-9]+)`, addf)
44 | suite.AddStep(`the result should equal ([+-]?[0-9]*[.]?[0-9]+)`, checkf)
45 |
46 | suite.Run()
47 | }
48 |
49 | func TestScenarioOutline(t *testing.T) {
50 | suite := NewSuite(t, WithFeaturesPath("features/outline.feature"))
51 | suite.AddStep(`I add (\d+) and (\d+)`, add)
52 | suite.AddStep(`the result should equal (\d+)`, check)
53 |
54 | suite.Run()
55 | }
56 |
57 | func TestParameterTypes(t *testing.T) {
58 | suite := NewSuite(t, WithFeaturesPath("features/parameter-types.feature"))
59 | suite.AddStep(`I add {int} and {int}`, add)
60 | suite.AddStep(`the result should equal {int}`, check)
61 | suite.AddStep(`I add floats {float} and {float}`, addf)
62 | suite.AddStep(`the result should equal float {float}`, checkf)
63 | suite.AddStep(`the result should equal text {text}`, checkt)
64 | suite.AddStep(`I use word {word}`, func(t StepTest, _ Context, word string) {
65 | if word != "pizza" {
66 | t.Fatal("it should be pizza")
67 | }
68 | })
69 | suite.AddStep(`I use text {text}`, func(_ StepTest, ctx Context, text string) {
70 | ctx.Set("stringRes", text)
71 | })
72 | suite.AddStep(`I concat word {word} and text {text}`, concat)
73 | suite.AddStep(`I format text {text} with int {int}`, func(_ StepTest, ctx Context, format string, value int) {
74 | ctx.Set("stringRes", fmt.Sprintf(format, value))
75 | })
76 |
77 | suite.Run()
78 | }
79 |
80 | func TestArguments(t *testing.T) {
81 | suite := NewSuite(t, WithFeaturesPath("features/argument.feature"))
82 | suite.AddStep(`the result should equal argument:`, checkt)
83 | suite.AddStep(`I concat text {text} and argument:`, concat)
84 |
85 | suite.Run()
86 | }
87 |
88 | func TestDatatable(t *testing.T) {
89 | suite := NewSuite(t, WithFeaturesPath("features/datatable.feature"))
90 | suite.AddStep(`I concat all the columns and row together using {text} to separate the columns`, concatTable)
91 | suite.AddStep(`the result should equal argument:`, checkt)
92 |
93 | suite.Run()
94 | }
95 |
96 | func TestScenarioOutlineExecutesAllTests(t *testing.T) {
97 | c := 0
98 | suite := NewSuite(t, WithFeaturesPath("features/outline.feature"))
99 | suite.AddStep(`I add (\d+) and (\d+)`, add)
100 | suite.AddStep(`the result should equal (\d+)`, func(t StepTest, ctx Context, sum int) {
101 | c++
102 | check(t, ctx, sum)
103 | })
104 |
105 | suite.Run()
106 |
107 | if err := assert.Equals(2, c); err != nil {
108 | t.Errorf("expected to run %d times but %d got", 2, c)
109 | }
110 | }
111 |
112 | func TestStepFromExample(t *testing.T) {
113 | s := NewSuite(t)
114 | st, expr := s.stepFromExample("I add and ", &msgs.TableRow{
115 | Cells: []*msgs.TableCell{
116 | {Value: "1"},
117 | {Value: "2"},
118 | },
119 | }, []string{"", ""})
120 |
121 | if err := assert.NotNil(st); err != nil {
122 | t.Error(err)
123 | }
124 |
125 | if err := assert.Equals("I add 1 and 2", st); err != nil {
126 | t.Error(err)
127 | }
128 |
129 | if err := assert.Equals(`I add (\d+) and (\d+)`, expr); err != nil {
130 | t.Error(err)
131 | }
132 | }
133 |
134 | func TestBackground(t *testing.T) {
135 | suite := NewSuite(t, WithFeaturesPath("features/background.feature"))
136 | suite.AddStep(`I add (\d+) and (\d+)`, add)
137 | suite.AddStep(`I concat word {word} and text {text}`, concat)
138 | suite.AddStep(`the result should equal text {text}`, checkt)
139 | suite.AddStep(`the result should equal (\d+)`, check)
140 |
141 | suite.Run()
142 | }
143 |
144 | func TestTags(t *testing.T) {
145 | suite := NewSuite(t, WithFeaturesPath("features/tags.feature"), WithTags("@tag"))
146 | suite.AddStep(`fail the test`, fail)
147 | suite.AddStep(`the test should pass`, pass)
148 |
149 | suite.Run()
150 | }
151 |
152 | func TestFilterFeatureWithTags(t *testing.T) {
153 | suite := NewSuite(t, WithFeaturesPath("features/filter_tags_*.feature"), WithTags("@run-this"))
154 | c := false
155 |
156 | suite.AddStep(`the test should pass`, func(_ StepTest, _ Context) {
157 | c = true
158 | })
159 | suite.AddStep(`fail the test`, fail)
160 |
161 | suite.Run()
162 |
163 | if err := assert.Equals(true, c); err != nil {
164 | t.Error(err)
165 | }
166 | }
167 |
168 | func TestWithAfterScenario(t *testing.T) {
169 | c := false
170 | suite := NewSuite(t, WithFeaturesPath("features/empty.feature"), WithAfterScenario(func(_ Context) {
171 | c = true
172 | }))
173 | suite.Run()
174 |
175 | if err := assert.Equals(true, c); err != nil {
176 | t.Error(err)
177 | }
178 | }
179 |
180 | func TestWithBeforeScenario(t *testing.T) {
181 | c := false
182 | suite := NewSuite(t, WithFeaturesPath("features/empty.feature"), WithBeforeScenario(func(_ Context) {
183 | c = true
184 | }))
185 | suite.Run()
186 |
187 | if err := assert.Equals(true, c); err != nil {
188 | t.Error(err)
189 | }
190 | }
191 |
192 | func TestWithAfterStep(t *testing.T) {
193 | c := 0
194 | suite := NewSuite(t, WithFeaturesPath("features/background.feature"), WithAfterStep(func(ctx Context) {
195 | c++
196 |
197 | // feature should be *msgs.Feature
198 | feature, err := ctx.Get(FeatureKey{})
199 | require.NoError(t, err)
200 | if _, ok := feature.(*msgs.Feature); !ok {
201 | t.Errorf("expected feature but got %T", feature)
202 | }
203 |
204 | // scenario should be *msgs.Scenario
205 | scenario, err := ctx.Get(ScenarioKey{})
206 | require.NoError(t, err)
207 | if _, ok := scenario.(*msgs.Scenario); !ok {
208 | t.Errorf("expected scenario but got %T", scenario)
209 | }
210 | }))
211 | suite.AddStep(`I add (\d+) and (\d+)`, add)
212 | suite.AddStep(`the result should equal (\d+)`, check)
213 | suite.AddStep(`I concat word {word} and text {text}`, concat)
214 | suite.AddStep(`the result should equal text {text}`, checkt)
215 |
216 | suite.Run()
217 |
218 | if err := assert.Equals(6, c); err != nil {
219 | t.Error(err)
220 | }
221 | }
222 |
223 | func TestWithBeforeStep(t *testing.T) {
224 | c := 0
225 | suite := NewSuite(t, WithFeaturesPath("features/background.feature"), WithBeforeStep(func(_ Context) {
226 | c++
227 | }))
228 | suite.AddStep(`I add (\d+) and (\d+)`, add)
229 | suite.AddStep(`the result should equal (\d+)`, check)
230 | suite.AddStep(`I concat word {word} and text {text}`, concat)
231 | suite.AddStep(`the result should equal text {text}`, checkt)
232 |
233 | suite.Run()
234 |
235 | if err := assert.Equals(6, c); err != nil {
236 | t.Error(err)
237 | }
238 | }
239 |
240 | func TestIgnoredTags(t *testing.T) {
241 | suite := NewSuite(t, WithFeaturesPath("features/ignored_*tags.feature"), WithIgnoredTags("@ignore"))
242 | suite.AddStep(`the test should pass`, pass)
243 | suite.AddStep(`fail the test`, fail)
244 | suite.Run()
245 | }
246 |
247 | func TestInvalidFunctionSignature(t *testing.T) {
248 | testCases := map[string]struct {
249 | f interface{}
250 | }{
251 | "nil": {},
252 | "func without return value": {f: func(_ Context) {}},
253 | "func with invalid return value": {f: func(_ Context) int { return 0 }},
254 | "func without arguments": {f: func() error { return errors.New("") }},
255 | "func with invalid first argument": {f: func(_ int) error { return errors.New("") }},
256 | }
257 |
258 | for name, testCase := range testCases {
259 | t.Run(name, func(t *testing.T) {
260 | tester := &mockTester{}
261 | suite := NewSuite(tester)
262 | suite.AddStep("", testCase.f)
263 | suite.Run()
264 | if err := assert.Equals(1, tester.fatalCalled); err != nil {
265 | t.Fatal(err)
266 | }
267 | })
268 | }
269 | }
270 |
271 | func TestFailureOutput(t *testing.T) {
272 | testCases := []struct {
273 | name string
274 | f interface{}
275 | expectedErrors []string
276 | }{
277 | {name: "passes", f: pass, expectedErrors: nil},
278 | {name: "returns error", f: failure, expectedErrors: []string{"the step failed"}},
279 | {name: "step panics", f: panics, expectedErrors: []string{"the step panicked"}},
280 | }
281 |
282 | for _, testCase := range testCases {
283 | t.Run(testCase.name, func(t *testing.T) {
284 | def := stepDef{f: testCase.f}
285 |
286 | tester := &mockTester{}
287 | def.run(NewContext(), tester, nil)
288 | err := assert.Equals(testCase.expectedErrors, tester.errors)
289 | if err != nil {
290 | t.Fatal(err)
291 | }
292 | })
293 | }
294 | }
295 |
296 | func addf(_ StepTest, ctx Context, var1, var2 float32) {
297 | res := var1 + var2
298 | ctx.Set("sumRes", res)
299 | }
300 |
301 | func add(_ StepTest, ctx Context, var1, var2 int) {
302 | res := var1 + var2
303 | ctx.Set("sumRes", res)
304 | }
305 |
306 | func concat(_ StepTest, ctx Context, var1, var2 string) {
307 | ctx.Set("stringRes", var1+var2)
308 | }
309 |
310 | func concatTable(_ StepTest, ctx Context, separator string, table msgs.DataTable) {
311 | rows := make([]string, 0, len(table.Rows))
312 | for _, row := range table.Rows {
313 | values := make([]string, 0, len(row.Cells))
314 | for _, cell := range row.Cells {
315 | values = append(values, cell.Value)
316 | }
317 |
318 | rows = append(rows, strings.Join(values, separator))
319 | }
320 |
321 | ctx.Set("stringRes", strings.Join(rows, "\n"))
322 | }
323 |
324 | func checkt(t StepTest, ctx Context, text string) {
325 | received, err := ctx.GetString("stringRes")
326 | if err != nil {
327 | t.Error(err.Error())
328 |
329 | return
330 | }
331 |
332 | if text != received {
333 | t.Errorf("expected %s but %s received", text, received)
334 | }
335 | }
336 |
337 | func checkf(t StepTest, ctx Context, sum float32) {
338 | received, err := ctx.Get("sumRes")
339 | if err != nil {
340 | t.Error(err.Error())
341 |
342 | return
343 | }
344 |
345 | if sum != received {
346 | t.Errorf("expected %f but %f received", sum, received)
347 | }
348 | }
349 |
350 | func check(t StepTest, ctx Context, sum int) {
351 | received, err := ctx.Get("sumRes")
352 | if err != nil {
353 | t.Error(err)
354 | return
355 | }
356 |
357 | if sum != received {
358 | t.Errorf("expected %d but %d received", sum, received)
359 | }
360 | }
361 |
362 | func fail(t StepTest, _ Context) {
363 | t.Error("the step should never be executed")
364 | }
365 |
366 | func failure(t StepTest, _ Context) {
367 | t.Error("the step failed")
368 | }
369 |
370 | func panics(_ StepTest, _ Context) {
371 | panic(errors.New("the step panicked"))
372 | }
373 |
374 | func pass(_ StepTest, _ Context) {}
375 |
376 | type mockTester struct {
377 | testing.T
378 | fatalCalled int
379 | errors []string
380 | }
381 |
382 | var _ TestingT = (*mockTester)(nil)
383 |
384 | func (m *mockTester) Log(...interface{}) {}
385 |
386 | func (m *mockTester) Logf(string, ...interface{}) {}
387 |
388 | func (m *mockTester) Fatal(...interface{}) {
389 | m.fatalCalled++
390 | }
391 |
392 | func (m *mockTester) Fatalf(string, ...interface{}) {}
393 |
394 | func (m *mockTester) Error(a ...interface{}) {
395 | m.errors = append(m.errors, fmt.Sprintf("%s", a...))
396 | }
397 |
398 | func (m *mockTester) Errorf(format string, a ...interface{}) {
399 | m.errors = append(m.errors, fmt.Sprintf(format, a...))
400 | }
401 |
402 | func (m *mockTester) Parallel() {}
403 |
404 | func (m *mockTester) Fail() {}
405 |
406 | func (m *mockTester) FailNow() {}
407 |
--------------------------------------------------------------------------------
/steps.go:
--------------------------------------------------------------------------------
1 | package gobdd
2 |
3 | import (
4 | "errors"
5 | "reflect"
6 | )
7 |
8 | func validateStepFunc(f interface{}) error {
9 | value := reflect.ValueOf(f)
10 | if value.Kind() != reflect.Func {
11 | return errors.New("the parameter should be a function")
12 | }
13 |
14 | if value.Type().NumIn() < contextArgumentsNumber {
15 | return errors.New("the function should have StepTest and Context as the first argument")
16 | }
17 |
18 | val := value.Type().In(0)
19 |
20 | testingInterface := reflect.TypeOf((*StepTest)(nil)).Elem()
21 | if !val.Implements(testingInterface) {
22 | return errors.New("the function should have the StepTest as the first argument")
23 | }
24 |
25 | val = value.Type().In(1)
26 |
27 | n := val.ConvertibleTo(reflect.TypeOf((*Context)(nil)).Elem())
28 | if !n {
29 | return errors.New("the function should have Context as the second argument")
30 | }
31 |
32 | return nil
33 | }
34 |
--------------------------------------------------------------------------------
/steps_context_test.go:
--------------------------------------------------------------------------------
1 | package gobdd_test
2 |
3 | import (
4 | "testing"
5 |
6 | gobdd "github.com/go-bdd/gobdd"
7 | "github.com/go-bdd/gobdd/context"
8 | )
9 |
10 | func TestValidateStepFunc_Context(t *testing.T) {
11 | testCases := map[string]interface{}{
12 | "function with invalid first argument": func(int, context.Context) {},
13 | }
14 |
15 | for name, testCase := range testCases {
16 | t.Run(name, func(t *testing.T) {
17 | if err := gobdd.ValidateStepFunc(testCase); err == nil {
18 | t.Errorf("the test should fail for the function")
19 | }
20 | })
21 | }
22 | }
23 |
24 | func TestValidateStepFunc_ValidFunction_Context(t *testing.T) {
25 | if err := gobdd.ValidateStepFunc(func(gobdd.StepTest, context.Context) {}); err != nil {
26 | t.Errorf("the test should NOT fail for the function: %s", err)
27 | }
28 | }
29 |
30 | func TestValidateStepFunc_ReturnContext_Context(t *testing.T) {
31 | err := gobdd.ValidateStepFunc(func(gobdd.StepTest, context.Context) context.Context { return context.Context{} })
32 | if err != nil {
33 | t.Errorf("step function returning a context should NOT fail validation: %s", err)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/steps_test.go:
--------------------------------------------------------------------------------
1 | package gobdd
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestValidateStepFunc(t *testing.T) {
8 | testCases := map[string]interface{}{
9 | "function without arguments": func() {},
10 | "function with 1 argument": func(StepTest) {},
11 | "function with invalid first argument": func(int, Context) {},
12 | }
13 |
14 | for name, testCase := range testCases {
15 | t.Run(name, func(t *testing.T) {
16 | if err := validateStepFunc(testCase); err == nil {
17 | t.Errorf("the test should fail for the function")
18 | }
19 | })
20 | }
21 | }
22 |
23 | func TestValidateStepFunc_ValidFunction(t *testing.T) {
24 | if err := validateStepFunc(func(StepTest, Context) {}); err != nil {
25 | t.Errorf("the test should NOT fail for the function: %s", err)
26 | }
27 | }
28 |
29 | func TestValidateStepFunc_ReturnContext(t *testing.T) {
30 | if err := validateStepFunc(func(StepTest, Context) Context { return Context{} }); err != nil {
31 | t.Errorf("step function returning a context should NOT fail validation: %s", err)
32 | }
33 | }
34 |
35 | // Used for context package backwards compatibility tests.
36 | func ValidateStepFunc(f interface{}) error {
37 | return validateStepFunc(f)
38 | }
39 |
--------------------------------------------------------------------------------