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.

🎯 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
# 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
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
// 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
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
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 usersfunc (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
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
// 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 5API Testing
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
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
# 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 --randomConfiguration File
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
# .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.featureStep Definition Patterns
// 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
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
# Install Godog
go install github.com/cucumber/godog/cmd/godog@latest
# Verify installation
godog versionInitialize Project
# 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 --initRun Tests
# 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
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
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
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
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