├── .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 | [![GoDoc](https://godoc.org/github.com/go-bdd/gobdd?status.svg)](https://godoc.org/github.com/go-bdd/gobdd) [![Coverage Status](https://coveralls.io/repos/github/go-bdd/gobdd/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------