GoBDD
Buy me a coffeeBuy me a coffee


GoBDD is a library that helps writing BDD tests in Go projects using Gherkin syntax. It’s an alternative to godog. There are a few things that differ and were my motivation to write my own library.

How does GoBDD work?

You write your tests using the gherkin syntax. All tests (by default) should be placed in features/ folder and have a prefix .feature.

The library reads all available documents. 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.

You can have multiple gherkin documents executed within one test.

What’s the difference compared to godog?

Godog runs as an external binary. You have to download it and run as a separate step in your project. It generates a temporary test file, compiles it and runs in an external proces. This way of doings it has some disadvantages:

That’s why I decided to choose another approach and build GoBDD as an extension to built-in testing library. It uses a regular and well-known testing.T.

func TestScenarios(t *testing.T) {
	suite := gobdd.NewSuite(t)
	// add your steps here
	suite.Run()
}

The second design decision I didn’t like about godog is using the global state. Let’s take a look at the example from their readme file.

func thereAreGodogs(available int) error {
	Godogs = available
	return nil
}

func iEat(num int) error {
	if Godogs < num {
		return fmt.Errorf("you cannot eat %d godogs, there are %d available", num, Godogs)
	}
	Godogs -= num
	return nil
}

func thereShouldBeRemaining(remaining int) error {
	if Godogs != remaining {
		return fmt.Errorf("expected %d godogs to be remaining, but there is %d", remaining, Godogs)
	}
	return nil
}

func InitializeTestSuite(ctx *godog.TestSuiteContext) {
	ctx.BeforeSuite(func() { Godogs = 0 })
}

func InitializeScenario(ctx *godog.ScenarioContext) {
	ctx.BeforeScenario(func(*godog.Scenario) {
		Godogs = 0 // clean the state before every scenario
	})

	ctx.Step(`^there are (\d+) godogs$`, thereAreGodogs)
	ctx.Step(`^I eat (\d+)$`, iEat)
	ctx.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
}

In those tests, the number of godogs is kept into a global variable. It means that it’s not safe to run those tests concurrently and there are some indirect dependencies between those steps. To fix the problem I decided to add a context.

func add(t gobdd.StepTest, ctx gobdd.Context, var1, var2 int) {
	res := var1 + var2
	ctx.Set("sumRes", res)
}

func check(t gobdd.StepTest, ctx gobdd.Context, sum int) {
	received, err := ctx.GetInt("sumRes")
    if err != nil {
        t.Error(err)

        return
    }

	if sum != received {
        t.Error(errors.New("the math does not work for you"))
	}
}

func TestScenarios(t *testing.T) {
	suite := NewSuite(t)
	suite.AddStep(`I add (\d+) and (\d+)`, add)
	suite.AddStep(`I the result should equal (\d+)`, check)
	suite.Run()
}

The context carriers the data from previously executed steps. It’s concurrent safe. There’s no need for creating a global variable. All you need lives in the Context.

GoBDD supports parameter types and have a built-in set of the most popular once.

    s := gobdd.NewSuite(t)
	s.AddParameterTypes(`{int}`, []string{`(\d)`})
	s.AddParameterTypes(`{float}`, []string{`([-+]?\d*\.?\d*)`})
	s.AddParameterTypes(`{word}`, []string{`([\d\w]+)`})
	s.AddParameterTypes(`{text}`, []string{`"([\d\w\-\s]+)"`, `'([\d\w\-\s]+)'`})

Thanks to this you can write

suite.AddStep(`I add {int} and {int}`, add)

instead of

suite.AddStep(`I add (\d+) and (\d+)`, add)

This feature helps writing more readable tests. You can, of course, write your own parameter types using the AddParameterTypes function.

GoBDD has much more features and don’t wait to try them. I’ll be thankful for any kind of contribution - feeling a bug report, writing a suggestion or creating a PR to any existing issue.

PS. If you like the project, tweet about it, share on FB or just tell your friends. I’ll be thankful!