Skip to main content

Echo Test Utilities

Testing is a crucial part of developing robust and reliable web applications. When working with Echo, Go's high-performance web framework, having the right test utilities makes the difference between cumbersome, fragile tests and clean, maintainable test suites. This guide introduces you to the essential test utilities for Echo applications.

Introduction to Echo Test Utilities

Echo Test Utilities are helper functions, structs, and methods that make it easier to write tests for Echo applications. They help you simulate HTTP requests, inspect responses, and verify that your handlers behave as expected without needing a running server.

The primary benefits of using Echo's test utilities include:

  • Testing HTTP handlers without starting a real server
  • Simulating various HTTP requests with different parameters and payloads
  • Inspecting response data, headers, and status codes
  • Creating isolated test environments

Core Echo Testing Components

Echo Test Context

The echo.Context interface is central to Echo applications. For testing, Echo provides a way to create a test context:

go
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

This creates a test context that you can pass to your handlers for testing.

HTTP Test Recorder

Go's standard library provides httptest.ResponseRecorder, which captures the response from your handlers:

go
rec := httptest.NewRecorder()

After your handler executes, you can inspect the recorder to verify the response:

go
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "Hello, World!", rec.Body.String())

Creating Test Requests

Basic Requests

To test a simple GET endpoint:

go
// Create a request
req := httptest.NewRequest(http.MethodGet, "/users", nil)
rec := httptest.NewRecorder()

// Set up the Echo instance and context
e := echo.New()
c := e.NewContext(req, rec)

// Call the handler
err := GetUsersHandler(c)

// Assertions
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)

Requests with Query Parameters

Testing endpoints with query parameters:

go
req := httptest.NewRequest(http.MethodGet, "/users?limit=10&offset=20", nil)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)

// Access query parameters in your handler
limit := c.QueryParam("limit")

Requests with Path Parameters

Path parameters are common in RESTful APIs. Here's how to test them:

go
// Create a request
req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
rec := httptest.NewRecorder()

// Set up the Echo instance and context
e := echo.New()
c := e.NewContext(req, rec)

// Set path parameters manually for testing
c.SetParamNames("id")
c.SetParamValues("123")

// Call your handler
err := GetUserByIDHandler(c)

Requests with JSON Body

Testing POST or PUT endpoints with JSON payloads:

go
// Create the request body
user := struct {
Name string `json:"name"`
Email string `json:"email"`
}{
Name: "John Doe",
Email: "[email protected]",
}
jsonBytes, _ := json.Marshal(user)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(jsonBytes))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)

// Call your handler
err := CreateUserHandler(c)

Testing Response Assertions

Status Code Verification

go
assert.Equal(t, http.StatusCreated, rec.Code)

Response Body Verification

For JSON responses:

go
// Parse the response
var responseUser struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
err := json.Unmarshal(rec.Body.Bytes(), &responseUser)
assert.NoError(t, err)

// Verify fields
assert.Equal(t, "John Doe", responseUser.Name)
assert.Equal(t, "[email protected]", responseUser.Email)
assert.NotEmpty(t, responseUser.ID)

Header Verification

go
assert.Equal(t, echo.MIMEApplicationJSON, rec.Header().Get(echo.HeaderContentType))

Creating Mock Services for Testing

Often your handlers depend on services or repositories. To test handlers in isolation, create mocks:

go
// UserService interface
type UserService interface {
GetUser(id string) (*User, error)
CreateUser(user *User) (*User, error)
}

// Mock implementation
type MockUserService struct {
GetUserFunc func(id string) (*User, error)
CreateUserFunc func(user *User) (*User, error)
}

func (m *MockUserService) GetUser(id string) (*User, error) {
return m.GetUserFunc(id)
}

func (m *MockUserService) CreateUser(user *User) (*User, error) {
return m.CreateUserFunc(user)
}

Using the mock in tests:

go
// Create the mock
mockService := &MockUserService{
GetUserFunc: func(id string) (*User, error) {
return &User{
ID: "123",
Name: "John Doe",
Email: "[email protected]",
}, nil
},
}

// Create handler with dependency injection
handler := NewUserHandler(mockService)

// Test the handler
req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
c.SetParamNames("id")
c.SetParamValues("123")

err := handler.GetUser(c)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)

Complete Example: Testing a User API

Let's put everything together with a complete example of testing a user API:

go
package main

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
)

// User represents our user model
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

// UserHandler handles user-related HTTP endpoints
type UserHandler struct {
service UserService
}

// UserService defines methods to handle user operations
type UserService interface {
GetUser(id string) (*User, error)
CreateUser(user *User) (*User, error)
}

// MockUserService is our test implementation
type MockUserService struct {
GetUserFunc func(id string) (*User, error)
CreateUserFunc func(user *User) (*User, error)
}

func (m *MockUserService) GetUser(id string) (*User, error) {
return m.GetUserFunc(id)
}

func (m *MockUserService) CreateUser(user *User) (*User, error) {
return m.CreateUserFunc(user)
}

// NewUserHandler creates a new UserHandler
func NewUserHandler(service UserService) *UserHandler {
return &UserHandler{service: service}
}

// GetUser handles GET /users/:id
func (h *UserHandler) GetUser(c echo.Context) error {
id := c.Param("id")
user, err := h.service.GetUser(id)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, user)
}

// CreateUser handles POST /users
func (h *UserHandler) CreateUser(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

user, err := h.service.CreateUser(u)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}

return c.JSON(http.StatusCreated, user)
}

func TestGetUser(t *testing.T) {
// Create mock service
mockService := &MockUserService{
GetUserFunc: func(id string) (*User, error) {
return &User{
ID: "123",
Name: "John Doe",
Email: "[email protected]",
}, nil
},
}

// Create handler
handler := NewUserHandler(mockService)

// Set up the test request
req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
c.SetParamNames("id")
c.SetParamValues("123")

// Call the handler
err := handler.GetUser(c)

// Test assertions
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)

// Check the response body
var user User
json.Unmarshal(rec.Body.Bytes(), &user)
assert.Equal(t, "123", user.ID)
assert.Equal(t, "John Doe", user.Name)
assert.Equal(t, "[email protected]", user.Email)
}

func TestCreateUser(t *testing.T) {
// Create mock service
mockService := &MockUserService{
CreateUserFunc: func(u *User) (*User, error) {
// Add an ID to simulate database creation
u.ID = "new-id-123"
return u, nil
},
}

// Create handler
handler := NewUserHandler(mockService)

// Create test request with JSON body
newUser := User{
Name: "Jane Doe",
Email: "[email protected]",
}
jsonBytes, _ := json.Marshal(newUser)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(jsonBytes))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)

// Call the handler
err := handler.CreateUser(c)

// Test assertions
assert.NoError(t, err)
assert.Equal(t, http.StatusCreated, rec.Code)

// Check the response body
var createdUser User
json.Unmarshal(rec.Body.Bytes(), &createdUser)
assert.Equal(t, "new-id-123", createdUser.ID)
assert.Equal(t, "Jane Doe", createdUser.Name)
assert.Equal(t, "[email protected]", createdUser.Email)
}

Testing Middleware

Echo middleware can also be tested:

go
func TestAuthMiddleware(t *testing.T) {
// Create middleware
authMiddleware := func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token != "valid-token" {
return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized")
}
return next(c)
}
}

// Create a simple handler
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "success")
}

// Test with valid token
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "valid-token")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

h := authMiddleware(handler)
err := h(c)

assert.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "success", rec.Body.String())

// Test with invalid token
req = httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "invalid-token")
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)

h = authMiddleware(handler)
err = h(c)

httpError, ok := err.(*echo.HTTPError)
assert.True(t, ok)
assert.Equal(t, http.StatusUnauthorized, httpError.Code)
}

Best Practices for Echo Testing

  1. Test in isolation: Use mocks for external dependencies to keep tests focused and fast.

  2. Test edge cases: Include tests for error conditions, not just the happy path.

  3. Group related tests: Use Go's subtests to organize tests for better clarity:

go
func TestUserHandler(t *testing.T) {
t.Run("GetUser", func(t *testing.T) {
// Test GetUser logic
})

t.Run("CreateUser", func(t *testing.T) {
// Test CreateUser logic
})
}
  1. Use table-driven tests for testing multiple variations:
go
func TestValidation(t *testing.T) {
tests := []struct {
name string
input User
expectedError string
}{
{
name: "Empty name",
input: User{Email: "[email protected]"},
expectedError: "name is required",
},
{
name: "Invalid email",
input: User{Name: "Test User", Email: "invalid"},
expectedError: "invalid email",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test each case
})
}
}
  1. Reuse setup code: Create helper functions for common setup tasks.

Summary

Echo's test utilities make it easier to write comprehensive tests for your web applications. By leveraging Go's testing packages alongside Echo's context mechanism, you can create reliable tests that verify your API's behavior without needing a running server.

Key points to remember:

  • Use httptest.NewRequest and httptest.NewRecorder to simulate HTTP interactions
  • Create test contexts with e.NewContext()
  • Set path parameters explicitly using c.SetParamNames() and c.SetParamValues()
  • Use mocks to isolate the code being tested
  • Assert on status codes, response bodies, and headers to verify behavior

Thorough testing with Echo's utilities helps catch bugs early and ensures your API behaves as expected when deployed.

Additional Resources

Exercises

  1. Write tests for a simple Echo API that has endpoints for listing, creating, updating, and deleting resources.

  2. Create a middleware that adds CORS headers and write tests to verify it works correctly.

  3. Write tests for an authentication system that uses JWT tokens.

  4. Create a test helper package with reusable functions for common testing tasks in your Echo applications.

  5. Practice using table-driven tests to verify validation logic for different API inputs.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)