Ginkgo

Gounit testingBDDbehavior driven developmentparallel testingGomegaDSLtest framework

Unit Testing Tool

Ginkgo

Overview

Ginkgo is a mature BDD (Behavior-Driven Development) testing framework for Go designed to help you write expressive specs. Built on top of Go's standard testing package and complemented by the Gomega matcher library, it shines in various testing contexts including unit tests, integration tests, acceptance tests, and performance tests. It's a general-purpose testing framework that adapts to your specific testing needs.

Details

Ginkgo is designed to coexist with Go's standard testing package, allowing you to mix existing Go tests. Currently, V2 is mainstream while V1 is no longer supported.

Key features of Ginkgo:

  • Expressive DSL: Organization through nested containers like Describe, Context, When, It
  • Powerful setup capabilities: Flexible setup with BeforeEach, AfterEach, BeforeSuite, AfterSuite
  • Parallel test execution: Sophisticated support for reproducible random order and spec parallelization
  • Command-line tools: Generation, execution, filtering, and profiling with the ginkgo command
  • Auto-watch functionality: Change detection and automatic execution with ginkgo watch
  • Gomega integration: Rich and mature family of assertions and matchers
  • Sync/async assertions: Mix synchronous and asynchronous assertions seamlessly
  • Focus and pending features: Focus tests with F prefix (FIt, FDescribe)
  • Timeout management: Custom timeout management with graceful interrupt handling

Ginkgo's philosophy centers on "independent specs," enabling randomization, filtering, and parallelization.

Advantages and Disadvantages

Advantages

  1. Advanced parallel execution: Significant time savings for large test suites
  2. Excellent readability: Natural language-like BDD syntax clarifies test intentions
  3. Rich toolset: Comprehensive command-line tools and features
  4. Gomega integration: Perfect integration with powerful matcher library
  5. Standard library coexistence: Seamless integration with existing Go tests
  6. Flexible test configuration: Flexible test control with focus, pending, and labels
  7. Maturity: Stable through years of development with proven track record
  8. Active development: Continuous feature additions and bug fixes

Disadvantages

  1. Learning cost: Understanding DSL-specific concepts and notation required
  2. Go only: Cannot be used with other programming languages
  3. Configuration complexity: Configuration can become complex in large projects
  4. Overhead: Heavier than standard testing package for small tests
  5. Dependencies: Dependency on external libraries like Gomega

Reference Pages

Usage Examples

Basic Setup

Installation

# Install Ginkgo CLI tool
go install github.com/onsi/ginkgo/v2/ginkgo@latest

# Install Gomega matcher library
go get github.com/onsi/gomega@latest

Project Initialization

# Initialize test suite
ginkgo bootstrap

# Generate new test file
ginkgo generate calculator

Basic Test Suite

// calculator_suite_test.go
package calculator_test

import (
    "testing"

    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
)

func TestCalculator(t *testing.T) {
    RegisterFailHandler(Fail)
    RunSpecs(t, "Calculator Suite")
}

Basic Test Structure

Using describe, context, it

// calculator_test.go
package calculator_test

import (
    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
    "myproject/calculator"
)

var _ = Describe("Calculator", func() {
    var calc *calculator.Calculator

    BeforeEach(func() {
        calc = calculator.New()
    })

    Describe("Add method", func() {
        Context("when adding positive numbers", func() {
            It("should return the correct sum", func() {
                result := calc.Add(2, 3)
                Expect(result).To(Equal(5))
            })

            It("should handle multiple numbers", func() {
                result := calc.Add(1, 2, 3, 4)
                Expect(result).To(Equal(10))
            })
        })

        Context("when adding negative numbers", func() {
            It("should return the correct negative sum", func() {
                result := calc.Add(-2, -3)
                Expect(result).To(Equal(-5))
            })
        })

        Context("when adding zero", func() {
            It("should return the other number", func() {
                Expect(calc.Add(5, 0)).To(Equal(5))
                Expect(calc.Add(0, 7)).To(Equal(7))
            })
        })
    })

    Describe("Divide method", func() {
        Context("when dividing by zero", func() {
            It("should return an error", func() {
                _, err := calc.Divide(10, 0)
                Expect(err).To(HaveOccurred())
                Expect(err.Error()).To(ContainSubstring("division by zero"))
            })
        })

        Context("when dividing valid numbers", func() {
            It("should return the correct quotient", func() {
                result, err := calc.Divide(10, 2)
                Expect(err).NotTo(HaveOccurred())
                Expect(result).To(Equal(5.0))
            })
        })
    })
})

Setup and Teardown

Test Lifecycle Management

var _ = Describe("Database Operations", func() {
    var db *database.Connection
    var testUser *models.User

    // Suite-wide setup (executed once)
    BeforeSuite(func() {
        db = database.Connect("test_database")
        Expect(db).NotTo(BeNil())
    })

    // Suite-wide teardown (executed once)
    AfterSuite(func() {
        db.Close()
    })

    // Before each spec (outer to inner order)
    BeforeEach(func() {
        testUser = &models.User{
            Name:  "Test User",
            Email: "[email protected]",
        }
    })

    // JustBeforeEach (after BeforeEach, just before It)
    JustBeforeEach(func() {
        err := db.Save(testUser)
        Expect(err).NotTo(HaveOccurred())
    })

    // After each spec (inner to outer order)
    AfterEach(func() {
        db.DeleteUser(testUser.ID)
    })

    // Cleanup registration
    BeforeEach(func() {
        DeferCleanup(func() {
            // This function runs after current spec or suite
            cleanupTempFiles()
        })
    })

    Describe("User Creation", func() {
        It("should create a user successfully", func() {
            users, err := db.GetAllUsers()
            Expect(err).NotTo(HaveOccurred())
            Expect(users).To(HaveLen(1))
            Expect(users[0].Name).To(Equal("Test User"))
        })
    })
})

Gomega Matchers

Basic Matchers

var _ = Describe("Gomega Matchers", func() {
    It("demonstrates basic matchers", func() {
        // Equality
        Expect(42).To(Equal(42))
        Expect("hello").To(Equal("hello"))
        
        // Boolean
        Expect(true).To(BeTrue())
        Expect(false).To(BeFalse())
        Expect(nil).To(BeNil())
        
        // Numerical comparison
        Expect(10).To(BeNumerically(">", 5))
        Expect(10).To(BeNumerically(">=", 10))
        Expect(5).To(BeNumerically("<", 10))
        Expect(3.14).To(BeNumerically("~", 3.1, 0.1))
    })

    It("demonstrates string matchers", func() {
        text := "Hello, Ginkgo World!"
        
        Expect(text).To(ContainSubstring("Ginkgo"))
        Expect(text).To(HavePrefix("Hello"))
        Expect(text).To(HaveSuffix("World!"))
        Expect(text).To(MatchRegexp(`Hello.*World`))
        Expect(text).NotTo(BeEmpty())
    })

    It("demonstrates collection matchers", func() {
        slice := []string{"apple", "banana", "cherry"}
        
        Expect(slice).To(HaveLen(3))
        Expect(slice).To(ContainElement("banana"))
        Expect(slice).To(ContainElements("apple", "cherry"))
        Expect(slice).To(ConsistOf("cherry", "apple", "banana"))
        Expect(slice).NotTo(BeEmpty())
        
        // Maps
        m := map[string]int{"a": 1, "b": 2}
        Expect(m).To(HaveKey("a"))
        Expect(m).To(HaveKeyWithValue("a", 1))
    })
})

Asynchronous Matchers

var _ = Describe("Asynchronous Testing", func() {
    It("tests asynchronous operations", func() {
        counter := 0
        
        // Eventually waits until condition is satisfied
        go func() {
            time.Sleep(100 * time.Millisecond)
            counter = 5
        }()
        
        Eventually(func() int {
            return counter
        }).Should(Equal(5))
        
        // Consistently verifies condition is maintained for duration
        Consistently(func() int {
            return counter
        }).Should(Equal(5))
    })

    It("tests channel operations", func() {
        ch := make(chan string, 1)
        
        go func() {
            time.Sleep(50 * time.Millisecond)
            ch <- "message"
        }()
        
        Eventually(ch).Should(Receive(Equal("message")))
    })
})

Parallel Testing and Focus

Parallel Execution

// Run tests in parallel
var _ = Describe("Parallel Tests", func() {
    It("runs in parallel", func() {
        // Get parallel process index
        processIndex := GinkgoParallelProcess()
        
        // Distribute resources across processes
        testData := getTestDataForProcess(processIndex)
        
        // Execute test
        result := processTestData(testData)
        Expect(result).To(BeTrue())
    })
})

// Synchronized setup across parallel processes
var _ = SynchronizedBeforeSuite(func() []byte {
    // Executed on only one process (heavy setup work)
    setupDatabase()
    return []byte("setup-complete")
}, func(data []byte) {
    // Executed on all processes (lightweight initialization)
    initializeClient()
})

var _ = SynchronizedAfterSuite(func() {
    // Executed on all processes
    cleanupClient()
}, func() {
    // Executed on only one process
    teardownDatabase()
})

Focus and Pending

var _ = Describe("Focus and Pending", func() {
    // F prefix makes only this test run
    FDescribe("Focused describe block", func() {
        It("will run", func() {
            Expect(true).To(BeTrue())
        })
    })

    Describe("Normal describe block", func() {
        FIt("focused test", func() {
            Expect(true).To(BeTrue())
        })

        It("normal test", func() {
            Expect(true).To(BeTrue())
        })
    })

    // P prefix or empty body makes test pending
    PIt("pending test", func() {
        // To be implemented
    })

    It("another pending test")  // Pending without body
})

Ordered Tests

Tests Requiring Order Guarantee

var _ = Describe("Integration Test", Ordered, func() {
    var server *testServer
    var client *httpClient

    BeforeAll(func() {
        server = startTestServer()
        client = createClient(server.URL)
    })

    AfterAll(func() {
        server.Stop()
    })

    It("should create a user", func() {
        user := &User{Name: "John", Email: "[email protected]"}
        err := client.CreateUser(user)
        Expect(err).NotTo(HaveOccurred())
    })

    It("should get the created user", func() {
        user, err := client.GetUser("[email protected]")
        Expect(err).NotTo(HaveOccurred())
        Expect(user.Name).To(Equal("John"))
    })

    It("should update the user", func() {
        err := client.UpdateUser("[email protected]", &User{Name: "John Doe"})
        Expect(err).NotTo(HaveOccurred())
    })

    It("should delete the user", func() {
        err := client.DeleteUser("[email protected]")
        Expect(err).NotTo(HaveOccurred())
    })
})

Labels and Filtering

Test Classification with Labels

var _ = Describe("API Tests", func() {
    It("tests basic functionality", Label("smoke", "api"), func() {
        // Smoke test
        Expect(apiClient.Ping()).To(Succeed())
    })

    It("tests advanced features", Label("integration", "api"), func() {
        // Integration test
        result := apiClient.ComplexOperation()
        Expect(result).To(BeTrue())
    })

    It("tests performance", Label("performance", "slow"), func() {
        // Performance test
        start := time.Now()
        apiClient.HeavyOperation()
        duration := time.Since(start)
        Expect(duration).To(BeNumerically("<", time.Second))
    })
})

Mocks and Stubs

Using Mocks with Ginkgo

var _ = Describe("Service Tests", func() {
    var (
        mockRepo *MockRepository
        service  *UserService
    )

    BeforeEach(func() {
        mockRepo = NewMockRepository()
        service = NewUserService(mockRepo)
    })

    Describe("GetUser", func() {
        Context("when user exists", func() {
            BeforeEach(func() {
                user := &User{ID: 1, Name: "John"}
                mockRepo.EXPECT().FindByID(1).Return(user, nil)
            })

            It("should return the user", func() {
                user, err := service.GetUser(1)
                Expect(err).NotTo(HaveOccurred())
                Expect(user.Name).To(Equal("John"))
            })
        })

        Context("when user does not exist", func() {
            BeforeEach(func() {
                mockRepo.EXPECT().FindByID(999).Return(nil, errors.New("not found"))
            })

            It("should return an error", func() {
                _, err := service.GetUser(999)
                Expect(err).To(HaveOccurred())
                Expect(err.Error()).To(ContainSubstring("not found"))
            })
        })
    })
})

Error Handling and Recovery

Error Handling in Goroutines

var _ = Describe("Goroutine Error Handling", func() {
    It("handles panics in goroutines", func() {
        done := make(chan bool)

        go func() {
            defer GinkgoRecover()  // Required: catch panics in goroutines

            // Some processing
            riskyOperation()
            
            Expect(someCondition).To(BeTrue())  // Panic on failure
            close(done)
        }()

        Eventually(done).Should(BeClosed())
    })
})

Command Line Operations

Using Ginkgo Commands

# Basic execution
ginkgo

# Parallel execution
ginkgo -p

# Randomization
ginkgo --randomize-all

# Execution with specific seed
ginkgo --seed=1234

# Focus execution
ginkgo --focus="Calculator.*Add"

# Label filtering
ginkgo --label-filter="smoke && !slow"

# Watch mode
ginkgo watch

# Execution with coverage
ginkgo --cover

# More verbose output
ginkgo -v

# Test specific directory
ginkgo ./pkg/calculator

# Build test binary
ginkgo build

# Profiling
ginkgo --cpuprofile=cpu.prof --memprofile=mem.prof

Custom Matchers

Creating Custom Matchers

// custom_matchers.go
package matchers

import (
    "fmt"
    "github.com/onsi/gomega/types"
)

func BeEvenNumber() types.GomegaMatcher {
    return &evenNumberMatcher{}
}

type evenNumberMatcher struct{}

func (matcher *evenNumberMatcher) Match(actual interface{}) (success bool, err error) {
    number, ok := actual.(int)
    if !ok {
        return false, fmt.Errorf("BeEvenNumber matcher expects an int")
    }
    
    return number%2 == 0, nil
}

func (matcher *evenNumberMatcher) FailureMessage(actual interface{}) (message string) {
    return fmt.Sprintf("Expected\n\t%v\nto be an even number", actual)
}

func (matcher *evenNumberMatcher) NegatedFailureMessage(actual interface{}) (message string) {
    return fmt.Sprintf("Expected\n\t%v\nnot to be an even number", actual)
}

// Usage example
var _ = Describe("Custom Matchers", func() {
    It("uses custom even number matcher", func() {
        Expect(4).To(BeEvenNumber())
        Expect(3).NotTo(BeEvenNumber())
    })
})