Godog is a Behavior-Driven Development (BDD) testing framework for Go that enables writing human-readable tests using Gherkin syntax, bridging the gap between technical and business stakeholders.

Godog is a Behavior-Driven Development (BDD) testing framework for the Go programming language. It enables developers and stakeholders to write human-readable tests using Gherkin syntax, fostering collaboration between technical teams and business stakeholders while providing robust testing capabilities.

Godog BDD Framework

🎯 Core Concepts

Behavior-Driven Development

  • Human-readable test scenarios
  • Business language specifications
  • Collaboration between teams
  • Living documentation through tests

Gherkin Syntax

  • Given-When-Then structure
  • Natural language descriptions
  • Executable specifications
  • Documentation as code

Go Integration

  • Native Go implementation
  • Type safety and performance
  • Go testing ecosystem compatibility
  • Concurrent execution support

🚀 Key Features

Gherkin Feature Files

gherkin
# features/user_registration.feature
Feature: User Registration
  As a new user
  I want to register an account
  So that I can access the application

  Scenario: Successful user registration
    Given I am on the registration page
    When I enter valid registration details
    And I submit the registration form
    Then I should be redirected to the dashboard
    And I should receive a welcome email

  Scenario Outline: Registration validation
    Given I am on the registration page
    When I enter "" as email
    And I enter "" as password
    And I submit the registration form
    Then I should see the error ""

    Examples:
      | email          | password | error_message          |
      | invalid-email  | pass123  | Invalid email format   |
      | user@test.com  | short    | Password too short     |
      |                | pass123  | Email is required      |

Step Definitions

go
package steps

import (
    "context"
    "testing"
    "github.com/cucumber/godog"
    "github.com/stretchr/testify/assert"
)

type registrationContext struct {
    email    string
    password string
    errors   []string
}

func (ctx *registrationContext) iAmOnTheRegistrationPage() error {
    // Navigate to registration page
    return nil
}

func (ctx *registrationContext) iEnterValidRegistrationDetails() error {
    ctx.email = "user@example.com"
    ctx.password = "securepassword123"
    return nil
}

func (ctx *registrationContext) iEnterAsEmail(email string) error {
    ctx.email = email
    return nil
}

func (ctx *registrationContext) iEnterAsPassword(password string) error {
    ctx.password = password
    return nil
}

func (ctx *registrationContext) iSubmitTheRegistrationForm() error {
    // Simulate form submission
    if ctx.email == "" {
        ctx.errors = append(ctx.errors, "Email is required")
    }
    if len(ctx.password) < 8 {
        ctx.errors = append(ctx.errors, "Password too short")
    }
    // Add more validation logic
    return nil
}

func (ctx *registrationContext) iShouldBeRedirectedToTheDashboard() error {
    // Assert redirection to dashboard
    return nil
}

func (ctx *registrationContext) iShouldReceiveAWelcomeEmail() error {
    // Assert welcome email was sent
    return nil
}

func (ctx *registrationContext) iShouldSeeTheError(errorMessage string) error {
    assert.Contains(ctx.errors, errorMessage)
    return nil
}

func InitializeScenario(ctx *godog.ScenarioContext) {
    regCtx := ®istrationContext{}

    ctx.Step(`^I am on the registration page$`, regCtx.iAmOnTheRegistrationPage)
    ctx.Step(`^I enter valid registration details$`, regCtx.iEnterValidRegistrationDetails)
    ctx.Step(`^I enter "([^"]*)" as email$`, regCtx.iEnterAsEmail)
    ctx.Step(`^I enter "([^"]*)" as password$`, regCtx.iEnterAsPassword)
    ctx.Step(`^I submit the registration form$`, regCtx.iSubmitTheRegistrationForm)
    ctx.Step(`^I should be redirected to the dashboard$`, regCtx.iShouldBeRedirectedToTheDashboard)
    ctx.Step(`^I should receive a welcome email$`, regCtx.iShouldReceiveAWelcomeEmail)
    ctx.Step(`^I should see the error "([^"]*)"$`, regCtx.iShouldSeeTheError)
}

Test Runner

go
// godog_test.go
package main

import (
    "testing"
    "github.com/cucumber/godog"
    "github.com/cucumber/godog/cmd/godog/util"
)

func TestFeatures(t *testing.T) {
    suite := godog.TestSuite{
        ScenarioInitializer: InitializeScenario,
        Options: &godog.Options{
            Format:   "progress",
            Paths:    []string{"features"},
            TestingT: t,
        },
    }

    if suite.Run() != 0 {
        t.Fatal("non-zero status returned, failed to run feature tests")
    }
}

func TestMain(m *testing.M) {
    util.Main(nil, nil, nil, "godog")
}

🛠️ Advanced Features

Scenario Context

go
type apiContext struct {
    baseURL string
    client  *http.Client
    request *http.Request
    response *http.Response
    data     map[string]interface{}
}

func (ctx *apiContext) initializeScenario(sc *godog.Scenario) {
    ctx.baseURL = "https://api.example.com"
    ctx.client = &http.Client{Timeout: 30 * time.Second}
    ctx.data = make(map[string]interface{})
}

func (ctx *apiContext) aRequestTo(method, endpoint string) error {
    url := ctx.baseURL + endpoint
    req, err := http.NewRequest(method, url, nil)
    if err != nil {
        return err
    }
    ctx.request = req
    return nil
}

func (ctx *apiContext) iSendTheRequest() error {
    resp, err := ctx.client.Do(ctx.request)
    if err != nil {
        return err
    }
    ctx.response = resp
    return nil
}

func (ctx *apiContext) theResponseStatusShouldBe(expectedStatus int) error {
    if ctx.response.StatusCode != expectedStatus {
        return fmt.Errorf("expected status %d, got %d", expectedStatus, ctx.response.StatusCode)
    }
    return nil
}

Data Tables

gherkin
Scenario: User creation with different roles
  Given the following users exist:
    | name    | email             | role      |
    | Alice   | alice@test.com    | admin     |
    | Bob     | bob@test.com      | user      |
    | Charlie | charlie@test.com  | moderator |
  When I request the user list
  Then I should receive 3 users
go
func (ctx *userContext) theFollowingUsersExist(table *godog.Table) error {
    for _, row := range table.Rows {
        user := User{
            Name:  row.Cells[0].Value,
            Email: row.Cells[1].Value,
            Role:  row.Cells[2].Value,
        }
        // Create user in database
        if err := ctx.db.Create(&user).Error; err != nil {
            return err
        }
    }
    return nil
}

Hooks and Setup

go
func InitializeTestSuite(ctx *godog.TestSuiteContext) {
    // Setup database connection
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        panic(err)
    }

    // Run migrations
    db.AutoMigrate(&User{}, &Product{})

    ctx.AfterSuite(func() {
        // Cleanup after all tests
        db.Exec("DROP TABLE users")
        db.Exec("DROP TABLE products")
    })
}

func InitializeScenario(ctx *godog.ScenarioContext) {
    testCtx := &testContext{
        db: db,
    }

    ctx.BeforeScenario(func(sc *godog.Scenario) {
        // Setup before each scenario
        testCtx.cleanupDatabase()
    })

    ctx.AfterScenario(func(sc *godog.Scenario, err error) {
        // Cleanup after each scenario
        if err != nil {
            // Log error details
        }
    })

    // Register step definitions
    ctx.Step(`^the following users exist:$`, testCtx.theFollowingUsersExist)
    // ... more steps
}

📊 Testing Strategies

Unit Testing Integration

go
// Combine BDD with unit tests
func (ctx *calculatorContext) iAddAnd(expectedResult int) error {
    result := ctx.calculator.Add(ctx.a, ctx.b)
    ctx.result = result
    return nil
}

func (ctx *calculatorContext) theResultShouldBe(expectedResult int) error {
    if ctx.result != expectedResult {
        return fmt.Errorf("expected %d, got %d", expectedResult, ctx.result)
    }
    return nil
}

// Feature file
// Feature: Calculator
//   Scenario: Adding numbers
//     When I add 2 and 3
//     Then the result should be 5

API Testing

gherkin
Feature: REST API
  Scenario: Create new resource
    Given I set Content-Type header to application/json
    And I set request body to:
      """
      {
        "name": "Test Item",
        "description": "A test item"
      }
      """
    When I send POST request to /api/items
    Then the response status should be 201
    And the response should contain:
      """
      {
        "id": "#number",
        "name": "Test Item",
        "description": "A test item"
      }
      """

Database Testing

go
func (ctx *dbContext) iHaveAUserWith(userData *godog.Table) error {
    for _, row := range userData.Rows {
        user := &User{
            Name:  row.Cells[0].Value,
            Email: row.Cells[1].Value,
        }
        return ctx.db.Create(user).Error
    }
    return nil
}

func (ctx *dbContext) iQueryForUsers() error {
    return ctx.db.Find(&ctx.users).Error
}

func (ctx *dbContext) iShouldHaveUsers(count int) error {
    if len(ctx.users) != count {
        return fmt.Errorf("expected %d users, got %d", count, len(ctx.users))
    }
    return nil
}

🔧 Configuration & Execution

Command Line Options

bash
# Run all features
godog

# Run specific feature
godog features/user_registration.feature

# Run with different formatters
godog --format=pretty
godog --format=junit > results.xml

# Run with tags
godog --tags=@smoke
godog --tags="~@slow"

# Parallel execution
godog --concurrency=4

# Random execution order
godog --random

Configuration File

go
func FeatureContext(s *godog.Suite) {
    opts := godog.Options{
        Output:        os.Stdout,
        Format:        "progress",
        StopOnFailure: false,
        Strict:        true,
        NoColors:      false,
        Paths:         []string{"features"},
        Tags:          "~@wip",
        Concurrency:   runtime.NumCPU(),
        Randomize:     time.Now().UTC().UnixNano(),
    }

    godog.BindCommandLineFlags("godog.", &opts)
    s.Options = &opts
}

CI/CD Integration

yaml
# .github/workflows/test.yml
name: BDD Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v4
        with:
          go-version: '1.21'
      - run: go test -v ./...
      - run: godog --format=junit > test-results.xml
      - uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: test-results.xml

🎯 Best Practices

Feature Organization

features/
├── auth/
│   ├── login.feature
│   ├── registration.feature
│   └── password_reset.feature
├── user_management/
│   ├── profile.feature
│   └── permissions.feature
└── api/
    ├── rest_api.feature
    └── graphql.feature

Step Definition Patterns

go
// Use dependency injection
type context struct {
    service ServiceInterface
    result  interface{}
}

func (c *context) someStep() error {
    result, err := c.service.DoSomething()
    c.result = result
    return err
}

// Use table-driven tests
func (c *context) validateTable(table *godog.Table) error {
    for i, row := range table.Rows {
        if i == 0 { // Skip header
            continue
        }
        // Validate row data
    }
    return nil
}

Error Handling

go
func (c *context) stepShouldSucceed() error {
    if err := c.lastError; err != nil {
        return fmt.Errorf("expected success, got error: %w", err)
    }
    return nil
}

func (c *context) stepShouldFailWithError(expectedError string) error {
    if c.lastError == nil {
        return errors.New("expected error, but operation succeeded")
    }
    if !strings.Contains(c.lastError.Error(), expectedError) {
        return fmt.Errorf("expected error containing '%s', got '%s'", expectedError, c.lastError.Error())
    }
    return nil
}

🚀 Getting Started

Installation

bash
# Install Godog
go install github.com/cucumber/godog/cmd/godog@latest

# Verify installation
godog version

Initialize Project

bash
# Create feature directory
mkdir features

# Create first feature file
cat > features/example.feature << EOF
Feature: Example feature
  Scenario: Example scenario
    Given I have a step
    When I run the step
    Then I should see the result
EOF

# Generate step definitions
godog --init

Run Tests

bash
# Run all features
godog

# Run with verbose output
godog --format=pretty

# Run specific feature
godog features/example.feature

# Generate step snippets
godog --format=suggestions

🔮 Advanced Usage

Custom Formatters

go
type customFormatter struct {
    // Custom formatter implementation
}

func (f *customFormatter) TestRunStarted() {}
func (f *customFormatter) Feature()        {}
func (f *customFormatter) Scenario()      {}
func (f *customFormatter) Step()          {}
func (f *customFormatter) Summary()       {}

func main() {
    formatter := &customFormatter{}
    godog.RunWithOptions("godog", func(s *godog.Suite) {
        s.Formatter(formatter)
    }, godog.Options{})
}

Parallel Execution

go
func InitializeTestSuite(ctx *godog.TestSuiteContext) {
    ctx.BeforeSuite(func() {
        // Setup shared resources
    })

    ctx.AfterSuite(func() {
        // Cleanup shared resources
    })
}

func TestFeatures(t *testing.T) {
    for i := 0; i < runtime.NumCPU(); i++ {
        t.Run(fmt.Sprintf("Suite%d", i), func(t *testing.T) {
            suite := godog.TestSuite{
                ScenarioInitializer: InitializeScenario,
                Options: &godog.Options{
                    Format:      "progress",
                    Paths:       []string{"features"},
                    TestingT:    t,
                    Concurrency: 1, // Each suite runs sequentially
                },
            }
            if suite.Run() != 0 {
                t.Fatal("test suite failed")
            }
        })
    }
}

📈 Integration Examples

Web Application Testing

go
type webContext struct {
    page *agouti.Page
}

func (ctx *webContext) iNavigateTo(path string) error {
    return ctx.page.Navigate("http://localhost:8080" + path)
}

func (ctx *webContext) iClickOn(text string) error {
    return ctx.page.FindByLinkText(text).Click()
}

func (ctx *webContext) iShouldSee(text string) error {
    return ctx.page.FindByText(text).ShouldBeFound()
}

Microservice Testing

go
type serviceContext struct {
    client *http.Client
    baseURL string
    response *http.Response
}

func (ctx *serviceContext) iSendRequestTo(method, path string) error {
    req, err := http.NewRequest(method, ctx.baseURL+path, nil)
    if err != nil {
        return err
    }

    resp, err := ctx.client.Do(req)
    if err != nil {
        return err
    }
    ctx.response = resp
    return nil
}

func (ctx *serviceContext) theResponseCodeShouldBe(code int) error {
    if ctx.response.StatusCode != code {
        return fmt.Errorf("expected status %d, got %d", code, ctx.response.StatusCode)
    }
    return nil
}

🌟 Community & Ecosystem

Extensions & Plugins

  • godog-junit - JUnit XML output
  • godog-html - HTML reports
  • godog-json - JSON output
  • godog-allure - Allure reporting

Integration Libraries

  • Testify - Assertions library
  • Ginkgo - Alternative BDD framework
  • Agouti - WebDriver client
  • httpexpect - HTTP testing

Learning Resources

  • Official documentation - Comprehensive guides
  • Cucumber school - BDD learning
  • Go testing blog - Best practices
  • Community Slack - Support and discussion

Website: godog.io
GitHub: cucumber/godog
Documentation: godog.io/docs
Cucumber: cucumber.io

Join the Conversation

Comments section coming soon...