# Ginkgo Test Suites Structure and Organization ## Summary This resource covers best practices for structuring and organizing Ginkgo test suites in Go applications. Ginkgo is a mature testing framework for Go designed to help you write expressive specs that works hand-in-hand with the Gomega matcher library. The guide focuses on how many test suite files to create, how to organize them by test type (unit, integration, contract, etc.), and effective patterns for managing complex test hierarchies. ## Test Suite Structure Philosophy ### One Suite Per Package Rule When testing Go code, unit tests for a package typically reside within the same directory as the package and are named *_test.go. Ginkgo follows this convention. The general principle is: **One Ginkgo suite per Go package being tested** Each package should have: 1. **One suite bootstrap file**: `package_suite_test.go` 2. **Multiple spec files**: One or more `*_test.go` files containing the actual tests 3. **Shared test helpers**: Common setup and utility functions ### Basic Suite Structure ```text mypackage/ ├── handler.go # Production code ├── service.go # Production code ├── mypackage_suite_test.go # Suite bootstrap (TestMypackage function) ├── handler_test.go # Handler specs ├── service_test.go # Service specs └── integration_test.go # Integration specs (optional) ``` ## Suite Bootstrap File Every Ginkgo suite needs exactly one bootstrap file generated with `ginkgo bootstrap`: ```go // mypackage_suite_test.go package mypackage_test import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "testing" ) func TestMypackage(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Mypackage Suite") } ``` **Key Points:** - You should only ever call RunSpecs once and you can let Ginkgo worry about calling *testing.T for you - The bootstrap file contains the entry point for the test suite - Suite-level setup/teardown (`BeforeSuite`/`AfterSuite`) typically goes here ## Organizing Tests by Type ### Test Type Categories Based on the test pyramid approach, organize your tests into these categories: #### 1. Unit Tests (Foundation - Primary Focus) - **Purpose**: Validate individual components in isolation with maximum execution speed and comprehensive coverage - **Speed**: Fast (thousands of tests in under 60 seconds) - **Testing Scope**: Business logic, component methods, data processing, input validation, edge cases - **Dependencies**: External dependencies replaced with test doubles - **File naming**: `*_test.go` in unit test directories #### 2. Feature Tests (Complete User Workflow Validation) - **Purpose**: Verify end-to-end user scenarios work correctly with minimal but comprehensive coverage - **Speed**: Medium to slow - **Testing Scope**: Critical user interaction paths, complete request/response cycles, authentication workflows - **Dependencies**: Test from user perspective through actual interfaces - **File naming**: `*_test.go` in feature test directories #### 3. Integration Tests (Targeted Boundary Testing) - **Purpose**: Validate complex integration points where unit and feature tests cannot provide adequate coverage - **Speed**: Medium - **When to Use**: Complex database operations, external API integration, file system operations, message queues - **When NOT to Use**: Simple CRUD operations, core business logic, basic API calls - **File naming**: `*_test.go` in integration test directories #### 4. Contract Tests (Service Interface Agreements) - **Purpose**: Ensure API contracts between services remain stable and functional across changes - **Speed**: Fast to medium - **When Essential**: Multi-service architectures, external API dependencies, team coordination - **Implementation**: Consumer-driven contract testing using tools like Pact - **File naming**: `*_test.go` in contract test directories ### Running Tests by Type Use directory structure and labels to run specific test types: ```bash # Rapid Feedback (Unit Tests) - < 2 minutes ginkgo -r ./pkg/*/tests/unit/... # Component Validation ginkgo -r ./pkg/api/tests/... # Integration Validation - < 10 minutes ginkgo -r ./pkg/*/tests/integration/... # System Confidence - < 30 minutes ginkgo -r ./pkg/*/tests/feature/... # Contract validation ginkgo -r ./pkg/*/tests/contract/... ``` ### Test Organization with Directory Structure and Labels Organize tests primarily by directory structure, with labels for additional categorization: ```go // In pkg/api/tests/unit/handlers/user_handler_test.go var _ = Describe("User Handler", Label("unit", "handlers"), func() { // Unit tests for user handler logic }) // In pkg/api/tests/feature/user_registration_test.go var _ = Describe("User Registration Flow", Label("feature", "critical-path"), func() { // Complete user registration workflow tests }) // In pkg/api/tests/integration/database_test.go var _ = Describe("Database Repository", Label("integration", "database"), func() { // Complex database operation tests }) // In pkg/api/tests/contract/external_api_test.go var _ = Describe("Payment Service Contract", Label("contract", "external-api"), func() { // API contract validation tests }) ``` Run with directory structure and labels: ```bash # Run by directory structure (primary method) ginkgo -r ./pkg/*/tests/unit/... ginkgo -r ./pkg/*/tests/feature/... # Run with additional label filtering ginkgo -r --label-filter="critical-path" ./pkg/*/tests/feature/... ginkgo -r --label-filter="database" ./pkg/*/tests/integration/... ``` ## Project Organization Patterns ### Single Component Projects For applications with unified codebases: ```text backend/tests/ ├── unit/ │ ├── handlers/ │ ├── services/ │ ├── models/ │ └── utils/ ├── feature/ │ ├── user_registration_flow_test.go │ ├── content_processing_workflow_test.go │ └── api_error_handling_test.go ├── integration/ │ ├── database_repository_test.go │ ├── external_payment_api_test.go │ └── file_upload_handler_test.go ├── contract/ │ ├── user_service_consumer_test.go │ └── payment_service_provider_test.go └── mocks/ └── # Test doubles and fixtures ``` ### Multi-Component Projects (Recommended) For complex applications with multiple independent components: ```text backend/pkg/ ├── api/ │ ├── api_suite_test.go # Bootstrap for API component │ └── tests/ │ ├── unit/ # API handlers, business logic │ ├── integration/ # External services, database repos │ ├── feature/ # Complete API user journeys │ ├── contract/ # API provider/consumer contracts │ └── mocks/ # API-specific test doubles ├── content-pipeline/ │ ├── pipeline_suite_test.go # Bootstrap for pipeline component │ └── tests/ │ ├── unit/ # Processing, transformations │ ├── integration/ # File systems, queues, database repos │ ├── feature/ # Complete pipeline workflows │ └── mocks/ # Pipeline-specific test doubles ├── database/ │ ├── database_suite_test.go # Bootstrap for database component │ └── tests/ │ ├── unit/ # Query builders, models, validation │ ├── integration/ # Raw DB operations, migrations │ └── mocks/ # Database-specific test doubles └── shared/ ├── shared_suite_test.go # Bootstrap for shared component └── tests/ ├── unit/ # Shared utility tests └── mocks/ # Truly shared test doubles ``` ## Advanced Suite Organization ### Hierarchical Test Structure Ginkgo allows you to hierarchically organize the specs in your suite using container nodes: ```go var _ = Describe("User Management System", func() { Describe("User Model", Label("unit"), func() { Context("Validation", func() { It("should validate email format", func() { // Test implementation }) }) Context("Serialization", func() { It("should marshal to JSON correctly", func() { // Test implementation }) }) }) Describe("User Service", Label("integration"), func() { BeforeEach(func() { // Setup database connection }) Context("Creating Users", func() { It("should create user in database", func() { // Test implementation }) }) }) }) ``` ### Shared Setup Patterns #### Suite-Level Setup ```go var ( dbClient *sql.DB server *httptest.Server ) var _ = BeforeSuite(func() { // Setup shared resources dbClient = setupTestDatabase() server = setupTestServer() DeferCleanup(func() { dbClient.Close() server.Close() }) }) ``` #### Test-Level Setup ```go var _ = Describe("User Service", func() { var ( userService *UserService user *User ) BeforeEach(func() { // Fresh setup for each test userService = NewUserService(dbClient) user = &User{ Email: "[email protected]", Name: "Test User", } }) AfterEach(func() { // Cleanup after each test cleanupTestData(dbClient) }) }) ``` ## Test Naming Conventions ### Naming Patterns for Different Test Types Use prefixes to identify test types: ```go // Unit tests var _ = Describe("UserService", Label("unit"), func() { It("should validate user email", func() {}) }) // Integration tests var _ = Describe("UserAPI Integration", Label("integration"), func() { It("should create user via API", func() {}) }) // Contract tests var _ = Describe("External API Contract", Label("contract"), func() { It("should match payment service schema", func() {}) }) ``` ### Alternative Naming with Test Functions For traditional Go test compatibility: ```go func TestUnit_UserValidation(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "User Validation Unit Tests") } func TestIntegration_UserAPI(t *testing.T) { if testing.Short() { t.Skip("Skipping integration tests in short mode") } RegisterFailHandler(Fail) RunSpecs(t, "User API Integration Tests") } ``` ## Running Different Test Types ### Development Workflow Commands **Rapid Feedback (Unit Tests) - Development:** ```bash # Single component during development ginkgo -r ./pkg/api/tests/unit/... # Specific functionality ginkgo -r ./pkg/api/tests/unit/handlers/... # All unit tests across components ginkgo -r ./pkg/*/tests/unit/... # With parallelization for speed ginkgo -r --procs=4 ./pkg/*/tests/unit/... ``` **Component Validation:** ```bash # Complete component test suite ginkgo -r ./pkg/api/tests/... # All integration tests ginkgo -r ./pkg/*/tests/integration/... # All feature tests ginkgo -r ./pkg/*/tests/feature/... ``` **Full System Validation:** ```bash # Complete test suite ginkgo -r ./pkg/*/tests/... # With additional filtering ginkgo -r --label-filter="critical-path" ./pkg/*/tests/... ``` ### CI/CD Pipeline Structure ```yaml # Example GitHub Actions workflow following test pyramid stages # Stage 1: Rapid Feedback (< 2 minutes) test-unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 - run: | ginkgo -r --procs=4 --race --cover ./pkg/*/tests/unit/... # Static analysis and linting go vet ./... golangci-lint run # Stage 2: Integration Validation (< 10 minutes) test-integration: runs-on: ubuntu-latest needs: test-unit services: postgres: image: postgres:13 redis: image: redis:6 strategy: matrix: component: [api, content-pipeline, database] steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 - run: | # Run integration tests by component (parallel execution) ginkgo -r ./pkg/${{ matrix.component }}/tests/integration/... # Contract test validation ginkgo -r ./pkg/${{ matrix.component }}/tests/contract/... # Stage 3: System Confidence (< 30 minutes) test-feature: runs-on: ubuntu-latest needs: test-integration strategy: matrix: component: [api, content-pipeline] steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 - run: | # Feature tests by component ginkgo -r ./pkg/${{ matrix.component }}/tests/feature/... # Performance regression testing go test -bench=. ./pkg/${{ matrix.component }}/... ``` ## Best Practices ### Suite Organization 1. **One suite per package**: Keep test suites focused on a single package 2. **Clear naming**: Use descriptive names for test files and specs 3. **Logical grouping**: Use container nodes to help clarify the intent behind your suite and group related specs together 4. **Separate concerns**: Keep unit, integration, and contract tests clearly distinguished ### Test Independence 1. **No shared state**: Ginkgo assumes specs are independent 2. **Fresh setup**: Use `BeforeEach` for test-specific setup 3. **Proper cleanup**: Use `AfterEach` or `DeferCleanup` for teardown 4. **Randomization**: By default, Ginkgo will randomize the order in which the specs in a suite run ### Performance Optimization 1. **Parallel execution**: Running specs in parallel is as easy as ginkgo -p 2. **Suite-level setup**: Use `BeforeSuite` for expensive setup operations 3. **Test categorization**: Run fast tests frequently, slow tests less often 4. **Resource cleanup**: Ensure proper cleanup to prevent resource leaks ### Maintainability 1. **Descriptive specs**: Use clear and concise descriptions for Describe and It blocks to ensure the intent of the tests is easily understood 2. **Helper functions**: Extract common test operations into helper functions 3. **Table-driven tests**: Use Ginkgo's table specs for testing multiple scenarios 4. **Documentation**: Document complex test setups and special requirements ## Common Anti-Patterns to Avoid ### 1. Multiple Suites per Package ```go // DON'T: Multiple bootstrap files in same package func TestUserService(t *testing.T) { /* ... */ } func TestUserRepository(t *testing.T) { /* ... */ } ``` ### 2. Monolithic Test Files ```go // DON'T: All tests in one massive file // user_service_suite_test.go (5000+ lines) ``` ### 3. Shared Mutable State ```go // DON'T: Shared variables that can be mutated var sharedUser = &User{} // This will cause test pollution ``` ### 4. Test Dependencies ```go // DON'T: Tests that depend on execution order It("should create user", func() { createdUser = createUser() // Setting global state }) It("should find created user", func() { Expect(findUser(createdUser.ID)).To(Succeed()) // Depends on previous test }) ``` ## Related Resources - [[Go API Testing Guide with Ginkgo + Gomega]] - Specific patterns for API testing - [[The Test Pyramid]] - Understanding different test types and their purposes - [Ginkgo Documentation](https://onsi.github.io/ginkgo/) - Official Ginkgo documentation - [Gomega Documentation](https://onsi.github.io/gomega/) - Gomega matcher library