DEV Community

Divya Darshana
Divya Darshana

Posted on

Mastering Testing in Go: Best Practices

Testing is the backbone of reliable software, and Go's simplicity makes it a perfect language for building robust test suites. When combined with GoFr—a modern Go framework designed for cloud-native applications—developers gain powerful tools to streamline testing workflows. This article explores essential testing best practices in Go, demonstrated through GoFr’s elegant testing capabilities.


Why Testing Matters in Go

  • Early bug detection
  • Refactoring confidence
  • Documentation through examples
  • Performance validation

GoFr enhances these benefits with:

  • Unified mocking for databases (SQL, Redis, Cassandra)
  • Declarative test expectations
  • Built-in HTTP testing utilities
  • Cross-service dependency management

1. Embrace Table-Driven Tests

Why: Test multiple scenarios with minimal code duplication.

GoFr Example:

func TestCreateBook(t *testing.T) {
    tests := []struct {
        desc         string
        payload      string
        mockSQLSetup func(mock sqlmock.Sqlmock)
        expectedCode int
    }{
        {
            desc:    "Success: Valid book creation",
            payload: `{"title":"GoFr in Action","isbn":9781234567897}`,
            mockSQLSetup: func(mock sqlmock.Sqlmock) {
                mock.ExpectExec("INSERT INTO books").
                    WithArgs("GoFr in Action", 9781234567897).
                    WillReturnResult(sqlmock.NewResult(1, 1))
            },
            expectedCode: http.StatusCreated,
        },
        {
            desc:         "Failure: Invalid ISBN format",
            payload:      `{"title":"Bad Book","isbn":"not-a-number"}`,
            mockSQLSetup: func(mock sqlmock.Sqlmock) {},
            expectedCode: http.StatusBadRequest,
        },
    }

    for _, tc := range tests {
        t.Run(tc.desc, func(t *testing.T) {
            // Initialize fresh mocks for isolation
            mockContainer, mock := container.NewMockContainer(t)
            tc.mockSQLSetup(mock.SQL)

            app := gofr.New()
            app.POST("/books", CreateBookHandler)

            req := httptest.NewRequest("POST", "/books", strings.NewReader(tc.payload))
            req.Header.Set("Content-Type", "application/json")

            resp := httptest.NewRecorder()
            app.ServeHTTP(resp, req)

            assert.Equal(t, tc.expectedCode, resp.Code)
            assert.True(t, mock.SQL.ExpectationsWereMet())
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Benefits:

  • Single test function validates multiple scenarios
  • Clear separation of test setup and assertions
  • Automatic mock expectation verification

2. Leverage Dependency Injection via Context

Why: Isolate components for true unit testing.

GoFr Implementation:

// Handler using GoFr's context
func GetUser(ctx *gofr.Context) (interface{}, error) {
    userID := ctx.PathParam("id")

    // Access Redis via context
    email, err := ctx.Redis.Get(ctx, "user:email:"+userID).Result()
    if err != nil {
        return nil, fmt.Errorf("redis error: %v", err)
    }

    return map[string]string{"email": email}, nil
}

// Test
func TestGetUser(t *testing.T) {
    mockContainer, mock := container.NewMockContainer(t)

    // Mock Redis response
    mock.Redis.ExpectGet("user:email:101").
        SetVal("user@example.com")

    ctx := &gofr.Context{
        Context:   context.Background(),
        Container: mockContainer,
        Request:   gofrHttp.NewRequest(httptest.NewRequest("GET", "/user/101", nil)),
    }

    result, err := GetUser(ctx)

    assert.Nil(t, err)
    assert.Equal(t, "user@example.com", result.(map[string]string)["email"])
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • No global dependencies
  • Easy mock configuration
  • Realistic context propagation testing

3. Isolate Tests with Fresh Instances

Why: Prevent test pollution between cases.

GoFr Pattern:

func TestOrderFlow(t *testing.T) {
    t.Parallel()

    t.Run("CreateOrder", func(t *testing.T) {
        t.Parallel()
        mockContainer, _ := container.NewMockContainer(t)
        app := gofr.New()

        // Test order creation...
    })

    t.Run("CancelOrder", func(t *testing.T) {
        t.Parallel()
        mockContainer, _ := container.NewMockContainer(t) // Fresh instance
        app := gofr.New()

        // Test cancellation...
    })
}
Enter fullscreen mode Exit fullscreen mode

Best Practices:

  • Use t.Parallel() for faster execution
  • Initialize new GoFr app instances per test
  • Separate mock containers for each subtest

4. Test Edge Cases & Failure Modes

Why: Ensure resilience under real-world conditions.

GoFr Failure Testing:

func TestDatabaseFailure(t *testing.T) {
    mockContainer, mock := container.NewMockContainer(t)

    // Simulate connection timeout
    mock.SQL.ExpectQuery("SELECT \* FROM products").
        WillReturnError(context.DeadlineExceeded)

    ctx := &gofr.Context{
        Context:   context.Background(),
        Container: mockContainer,
    }

    _, err := GetProductDetails(ctx)

    assert.ErrorContains(t, err, "context deadline exceeded")
}
Enter fullscreen mode Exit fullscreen mode

Critical Scenarios to Test:

  • Database connection failures
  • Network timeouts
  • Invalid input formats
  • Concurrent write collisions

5. Avoid Global State

Why: Ensure test determinism and reproducibility.

GoFr’s Containerized Approach:

func TestCartService(t *testing.T) {
    mockContainer, mock := container.NewMockContainer(t)

    // Mock cross-store interactions
    mock.SQL.ExpectBegin()
    mock.SQL.ExpectExec("INSERT INTO cart_items").
        WillReturnResult(sqlmock.NewResult(1, 1))
    mock.Redis.ExpectSet("cart:123", mock.Anything(), 30*time.Minute).
        SetVal("OK")

    // Test cart addition flow
    ctx := &gofr.Context{Container: mockContainer}
    err := AddToCart(ctx, "123", "item-456")

    assert.Nil(t, err)
    assert.True(t, mock.ExpectationsWereMet())
}
Enter fullscreen mode Exit fullscreen mode

Patterns to Follow:

  • Initialize fresh dependencies per test
  • Use mock containers for multi-store operations
  • Clean up resources in test teardown

6. Validate HTTP Contracts

Why: Ensure API spec compliance.

GoFr HTTP Testing:

func TestAPIEndpoints(t *testing.T) {
    app := gofr.New()
    app.GET("/products/{id}", GetProductHandler)

    t.Run("ValidProduct", func(t *testing.T) {
        mockContainer, mock := container.NewMockContainer(t)
        mock.SQL.ExpectQuery("SELECT \* FROM products").
            WithArgs("101").
            WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("GoFr Book"))

        req := httptest.NewRequest("GET", "/products/101", nil)
        resp := httptest.NewRecorder()

        app.ServeHTTP(resp, req)

        assert.Equal(t, http.StatusOK, resp.Code)
        assert.JSONEq(t, `{"name":"GoFr Book"}`, resp.Body.String())
    })
}
Enter fullscreen mode Exit fullscreen mode

Validation Checklist:

  • HTTP status codes
  • Response headers
  • JSON schema compliance
  • Error message formats

7. Benchmark Critical Paths

Why: Identify performance bottlenecks early.

GoFr Benchmark Example:

func BenchmarkProductSearch(b *testing.B) {
    mockContainer, _ := container.NewMockContainer(b)
    app := gofr.New()
    app.GET("/products/search", SearchProductsHandler)

    req := httptest.NewRequest("GET", "/products/search?q=golang", nil)

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        resp := httptest.NewRecorder()
        app.ServeHTTP(resp, req)
    }
}
Enter fullscreen mode Exit fullscreen mode

Optimization Insights:

  • Database query performance
  • Cache hit ratios
  • JSON serialization speed
  • Concurrent request handling

GoFr Testing Features Cheat Sheet

Feature Usage Example Benefit
Unified Mock Container container.NewMockContainer(t) Test SQL+Redis+Cassandra together
Declarative Expectations mock.SQL.ExpectQuery(...) Clear test intent
HTTP Service Mocking container.WithMockHTTPService(...) Test external API integrations
Automatic Validation mock.ExpectationsWereMet() Catch unmet dependencies
Contextual Logging ctx.Logger.Error(...) Debug test failures efficiently

Final Testing Checklist

  1. Cover success and error paths
  2. Validate all external interactions
  3. Test boundary conditions
  4. Run tests with -race flag
  5. Maintain >80% test coverage
  6. Include integration tests
  7. Update tests with feature changes

Conclusion: Building Confidence with GoFr

GoFr transforms Go testing from a chore into a strategic advantage by:

  1. Providing unified mocking across databases and services
  2. Enabling declarative test setup with clear expectations
  3. Supporting real-world scenario testing through containerized dependencies
  4. Offering built-in validation for test integrity

By adopting these practices with GoFr, teams can:

  • Reduce production incidents by 40-60%
  • Accelerate release cycles through reliable CI/CD
  • Improve system design via test-driven development
  • Maintain documentation through living test cases
// Start your testing journey today!
func TestYourApplication(t *testing.T) {
    // GoFr makes it this straightforward
    mockContainer, _ := container.NewMockContainer(t)
    app := gofr.New()

    // Your test logic here...
}
Enter fullscreen mode Exit fullscreen mode

Explore GoFr Testing Documentation

"Quality is never an accident—it’s always the result of intelligent effort."

― John Ruskin

If you are learning Golang or interested in

Top comments (0)