Ginkgo
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
ginkgocommand - 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
Fprefix (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
- Advanced parallel execution: Significant time savings for large test suites
- Excellent readability: Natural language-like BDD syntax clarifies test intentions
- Rich toolset: Comprehensive command-line tools and features
- Gomega integration: Perfect integration with powerful matcher library
- Standard library coexistence: Seamless integration with existing Go tests
- Flexible test configuration: Flexible test control with focus, pending, and labels
- Maturity: Stable through years of development with proven track record
- Active development: Continuous feature additions and bug fixes
Disadvantages
- Learning cost: Understanding DSL-specific concepts and notation required
- Go only: Cannot be used with other programming languages
- Configuration complexity: Configuration can become complex in large projects
- Overhead: Heavier than standard testing package for small tests
- Dependencies: Dependency on external libraries like Gomega
Reference Pages
- Official Documentation
- GitHub Repository
- Gomega Matcher Library
- Go Packages
- Getting Started with BDD in Go
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())
})
})