Mike Mwita


Behavior-Driven Development in Go

May 12, 2020 · 5 min read  #go #testing #bdd #godog

Testing is often seen as a technical safeguard — a way to ensure code correctness. But what if tests could do more than that? What if they could express intent, document behavior, and communicate meaning across developers, testers, and product owners alike?

Introduction #

That’s the promise of Behavior-Driven Development (BDD) — a practice that elevates testing from verifying logic to describing how the system behaves.

In Go, the tool that brings this philosophy to life is GoDog, the Cucumber-inspired framework for writing executable specifications using natural-language syntax.

Yet before diving into GoDog, here is my thoughton a long-standing misconception I have come across multiple times: BDD is not about Gherkin syntax — it’s a mindset.

BDD Is Not Gherkin — It’s a Mindset #

BDD was introduced by Dan North in 2003 as an evolution of Test-Driven Development (TDD). He noticed that teams often struggled with TDD because the term “test” implied verification, not behavioral design.

BDD reframed the question:

Instead of How do I test this function? ask What behavior am I expecting from the system?

If I was to summarize it in one sentence:

BDD is TDD done right.

Nothing stops you from writing non-BDD tests with Gherkin.The syntax doesn’t make your tests behavioral — your mindset does.

If your Gherkin scenarios simply mirror technical operations (click button, expect 200 OK), you’re missing the spirit of BDD. But when you use it to express intent (user should see confirmation when signup succeeds), you’re speaking the language of behavior.

So let’s explore how to do that right.

Understanding the Gherkin Syntax #

Gherkin is a structured, readable way to describe system behavior using a few key keywords:

KeywordPurposeExample
FeatureDescribes a system capabilityFeature: User authentication
ScenarioDescribes one example of behaviorScenario: Successful login
GivenPrecondition/setupGiven a user exists
WhenAction performedWhen the user logs in
ThenExpected outcomeThen they should see their dashboard
And / ButLogical connectorsAnd they should receive a welcome message

A Simple Example #

Here’s a simple example:

Feature: Shopping Basket
  Scenario: Adding a product
    Given I have an empty basket
    When I add "2" "Apples"
    Then the basket total should be "2"

Introducing GoDog #

GoDog is the Cucumber for Go — it executes your .feature files by mapping each step to Go functions called step definitions.

A typical GoDog project has this structure:

project/
├── features/
│   ├── signin.feature
│   └── signup.feature
├── steps/
│   └── auth_steps_test.go
└── go.mod

Below is a realworld BDD feature , referenced from one of my repo

signin.feature #

Feature: Signin Feature
  I want to Signin to Fedha API

  Background:
    Given user detail:
      |name         |email             |password|
      |Jonathan Doe |johndoe@mail.com  |abcdefg |
    Given I have load fedha application
    And the fields "email" and "password" are empty

  Scenario: Error on empty fields
    When I click on "Sign In"
    And both fields "email" and "password" have errors
    Then failed to login

  Scenario: Login Successful
    When I type "email" and "password"
    And I click on "Sign In"
    Then the user name should be "Jonathan Doe"

Implementing Step Definitions #

func (a *App) iClickOn(button string) error {
    if button == "Sign In" {
        a.response = a.client.SignIn(a.email, a.password)
    }
    return nil
}

func (a *App) userNameShouldBe(expected string) error {
    if a.user.Name != expected {
        return fmt.Errorf("expected user name %s, got %s", expected, a.user.Name)
    }
    return nil
}

And these are registered in the FeatureContext:

func FeatureContext(s *godog.Suite) {
    app := NewApp()
    s.Step(`^I click on "([^"]*)"$`, app.iClickOn)
    s.Step(`^the user name should be "([^"]*)"$`, app.userNameShouldBe)
}

When you run godog run, GoDog reads your .feature files and executes the mapped step definitions.

Managing Shared State Between Steps #

One of the trickiest challenges in BDD testing is sharing state between steps.

For example, your Given step might set up a user, and your When step needs to act on that same user. If you use global variables, you’ll quickly run into concurrency issues.

type SharedState struct {
    Email    string
    Password string
    Response string
}

var sharedKey = struct{}{}

func getSharedState(ctx context.Context) (context.Context, *SharedState) {
    v := ctx.Value(sharedKey)
    if v == nil {
        v = &SharedState{}
        ctx = context.WithValue(ctx, sharedKey, v)
    }
    return ctx, v.(*SharedState)
}

Then, use this inside your steps:

func givenUserDetail(ctx context.Context, name, email, password string) (context.Context, error) {
    ctx, state := getSharedState(ctx)
    state.Email = email
    state.Password = password
    return ctx, nil
}

func whenUserClicksSignIn(ctx context.Context) (context.Context, error) {
    ctx, state := getSharedState(ctx)
    state.Response = simulateLogin(state.Email, state.Password)
    return ctx, nil
}

func thenLoginShouldFail(ctx context.Context, expected string) (context.Context, error) {
    _, state := getSharedState(ctx)
    if state.Response != expected {
        return ctx, fmt.Errorf("expected %s, got %s", expected, state.Response)
    }
    return ctx, nil
}

Why BDD Matters #

BDD brings meaning back to testing. It’s not about fancy syntax or cucumber-green outputs — it’s about ensuring your system behaves as intended, in terms that humans and machines can both understand.

In Go, GoDog makes this possible without losing the language’s pragmatic simplicity.

Write tests that tell stories and let your behaviors speak louder than your assertions.