Testing your application is possible on so many levels. You start with unit tests for specific places in the source code, then you add integration tests that check interactions between different places in the system. You can wrap things up with some functional tests, which are either done manually or automatically using tools like Selenium. The problem is that you need tech knowledge for most of those steps. But what if I told you that you can give your business people, managers and project managers tool to add tests on their own? Say hello to BDD!

What is BDD

Behaviour Driven Development is a technique that evolved from TDD (Test Driven Development) with one significant difference, which is the language used to define tests cases. Obviously, I don't mean a programming language, but the natural language. We would like to allow less technical team members to define test cases as:

Given some initial state

When I perform some action

Then I get a specific result

This approach makes it easy eg. for the business people that work closer to the end users of your application to let you know what is the expected behavior and at the same time contribute to the codebase with those test suites, which make sure that the functionality is in place forever.

Cucumber and Gherkin

Cucumber is probably the most popular BDD tool available, with .NET-based SpecFlow being its biggest competitor. It comes with a language parser called Gherkin, that allows the framework to convert a natural language using regular expressions into the source code of our tests. Since we want to use Go, the framework that is recommended here is called godog, created by DataDog.

Installing godog

As stated in its README, DATA-DOG/godog is is the official Cucumber BDD framework for Golang as its author is a member of the Cucumber team. It follows the specification and acts similar to go test tool - it looks for test cases specifications in features/*.feature files, then matches them with FeatureXyz(s *godog.Suite) functions in *_test.go files and runs them as tests. In order to install the tool, all you need to do is type:

go get github.com/DATA-DOG/godog/cmd/godog

If your $PATH contains $GOBIN directory where Go binaries are being installed to by default, you should be able to run godog from now on:

$ godog -version 
Godog version is: v0.7.9

Testing HTTP server with godog

In order to check how BDD works with Golang, we will implement an HTTP server application with CRUD functionality for quotes. Each quote will have three fields:

type quote struct {
    ID     string `json:"id,omitempty"`
    Quote  string `json:"quote,omitempty"`
    Author string `json:"author,omitempty"`
}

The server will have in-memory storage (a map) and four endpoints, which are pretty self-explanatory:

func main() {
    svc := service{
        db: map[string]*quote{},
    }

    mux := chi.NewRouter()
    mux.Post("/quotes", svc.AddQuote)
    mux.Get("/quotes", svc.ListQuotes)
    mux.Get("/quotes/{id}", svc.GetQuote)
    mux.Delete("/quotes/{id}", svc.DeleteQuote)

    http.ListenAndServe(":8888", mux)
}

Now we should be able to create our first feature file:

# features/crud.feature
Feature: CRUD

    Scenario: create and get quote
        Given I create quote "xxx" by "aaa"
        When I ask for last created quote
        Then I should get "xxx" as quote
        And I should get "aba" as author

We can now run godog to see what steps should we take next:

$ godog
Feature: CRUD

Scenario: create and get quote        # features/crud.feature:3
    Given I create quote "xxx" by "aaa"
    When I ask for last created quote
    Then I should get "xxx" as quote
    And I should get "aaa" as author

1 scenarios (1 undefined)
4 steps (4 undefined)
100.955µs

You can implement step definitions for undefined steps with these snippets:

func iCreateQuoteBy(arg1, arg2 string) error {
        return godog.ErrPending
}

...

func FeatureContext(s *godog.Suite) {
        s.Step(`^I create quote "([^"]*)" by "([^"]*)"$`, iCreateQuoteBy)
        s.Step(`^I ask for last created quote$`, iAskForLastCreatedQuote)
        s.Step(`^I should get "([^"]*)" as quote$`, iShouldGetAsQuote)
        s.Step(`^I should get "([^"]*)" as author$`, iShouldGetAsAuthor)
}

So you can see how you need to actually implement the steps. The problem is, that they cannot be a bunch of independent functions, as one step relies on the previous ones. This is why we'll introduce a testContext struct that will have three fields:

type testContext struct {
    id    string
    quote quote
    cli   *apiClient
}

id and quote will represent the quote we will be creating (and retrieving later), and cli will represent an HTTP API client that will allow us to call our server. Next, we'll implement the functions suggested by godog output from above:

func (ctx *testContext) createQuote(quote, author string) error {
    id, err := ctx.cli.Create(quote, author)
    ...
    ctx.id = id
    return nil
}

func (ctx *testContext) askForLastCreatedQuote() error {
    q, err := ctx.cli.Get(ctx.id)
    ...
    ctx.quote = q
    return nil
}

func (ctx *testContext) shouldGetAsQuote(quote string) error {
    if want, got := quote, ctx.quote.Quote; want != got {
        return fmt.Errorf("expected quote '%s', got '%s'", want, got)
    }
    return nil
}

func (ctx *testContext) shouldGetAsAuthor(author string) error {
    if want, got := author, ctx.quote.Author; want != got {
        return fmt.Errorf("expected author '%s', got '%s'", want, got)
    }
    return nil
}

Last but not least, we'll use those functions in FeatureContext function that will be run by godog:

func FeatureContext(s *godog.Suite) {
    ctx := &testContext{
        cli: &apiClient{},
    }

    s.Step(`^I create quote "([^"]*)" by "([^"]*)"$`, ctx.createQuote)
    s.Step(`^I ask for last created quote$`, ctx.askForLastCreatedQuote)
    s.Step(`^I should get "([^"]*)" as quote$`, ctx.shouldGetAsQuote)
    s.Step(`^I should get "([^"]*)" as author$`, ctx.shouldGetAsAuthor)
}

Having done that, we can run our HTTP server, pass its URL into apiClient and finally re-run godog:

$ godog
Feature: CRUD

Scenario: create and get quote        # features/crud.feature:4
    Given I create quote "xxx" by "aaa" # main_test.go:93 -> *testContext
    When I ask for last created quote   # main_test.go:94 -> *testContext
    Then I should get "xxx" as quote    # main_test.go:95 -> *testContext
    And I should get "aaa" as author    # main_test.go:96 -> *testContext

1 scenarios (1 passed)
4 steps (4 passed)
3.703719ms

It works! We can also try to change the expectations in Then part to see if the test fails:

$ godog
Feature: CRUD

Scenario: create and get quote        # features/crud.feature:4
    Given I create quote "xxx" by "aaa" # main_test.go:93 -> *testContext
    When I ask for last created quote   # main_test.go:94 -> *testContext
    Then I should get "xxx" as quote    # main_test.go:95 -> *testContext
    And I should get "bbb" as author    # main_test.go:96 -> *testContext
    expected author 'bbb', got 'aaa'

--- Failed steps:

Scenario: create and get quote # features/crud.feature:4
    And I should get "bbb" as author # features/crud.feature:8
    Error: expected author 'bbb', got 'aaa'


1 scenarios (1 failed)
4 steps (3 passed, 1 failed)
3.548932ms

It does, just as we expected.

Summary

As you can see, it takes some time and effort to create an environment in your Go project to start writing BDD tests. At first, that might seem not worth it, but once you cover the most important possible action and verification steps, adding new test cases will be effortless for both developers and QA or business people in your team. This is when BDD reveals its true value and makes life easier for non-technical team members. If you have the possibility, time and resources to try BDD in your project, I would highly recommend testing it out.

Links

Versions

  • Go: go1.11.5
  • godog: v0.7.9