Skip to main content

Echo Unit Testing

Introduction

Unit testing is a fundamental practice in software development that involves testing individual components or "units" of code in isolation. In the context of Echo applications, unit testing allows you to verify that your API endpoints, middleware, and handlers behave as expected without needing to run the entire application.

In this guide, you'll learn how to write effective unit tests for your Echo applications. We'll cover setting up a testing environment, creating test cases for handlers, mocking dependencies, and ensuring your API behaves as expected.

Why Unit Testing Matters for Echo Applications

Before diving into the code, let's understand why unit testing is particularly important for Echo web applications:

  • API Reliability: Tests ensure your endpoints behave correctly under various conditions
  • Regression Prevention: Changes to your code won't break existing functionality
  • Documentation: Tests serve as working examples of how your API is supposed to function
  • Confidence: Well-tested code enables faster development with fewer bugs

Setting Up For Echo Unit Testing

To get started with Echo unit testing, you'll need to import the required packages:

go
import (
"net/http"
"net/http/httptest"
"strings"
"testing"

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

Here's what each of these packages is used for:

  • net/http and net/http/httptest: Create HTTP requests and record responses
  • strings: Manipulate strings (useful for request bodies)
  • testing: Go's built-in testing framework
  • github.com/labstack/echo/v4: The Echo web framework
  • github.com/stretchr/testify/assert: A helpful assertion library to make tests more readable

Basic Echo Unit Test Structure

Let's create a simple handler and its corresponding test:

go
// handlers.go
package handlers

import (
"github.com/labstack/echo/v4"
"net/http"
)

// HealthCheckHandler returns a simple status message
func HealthCheckHandler(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"status": "healthy",
})
}

Now, let's write a test for this handler:

go
// handlers_test.go
package handlers

import (
"net/http"
"net/http/httptest"
"testing"

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

func TestHealthCheckHandler(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Assertions
if assert.NoError(t, HealthCheckHandler(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "healthy")
}
}

This test follows a simple pattern:

  1. Create a new Echo instance
  2. Create a test HTTP request
  3. Create a response recorder
  4. Create an Echo context with the request and recorder
  5. Call the handler function directly
  6. Assert that the response matches expectations

Testing Handlers with Path Parameters

Echo makes it easy to define routes with path parameters. Here's how to test them:

go
// handlers.go
func GetUserHandler(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, map[string]string{
"id": id,
"name": "User " + id,
})
}

And its corresponding test:

go
func TestGetUserHandler(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Set path parameter
c.SetPath("/users/:id")
c.SetParamNames("id")
c.SetParamValues("123")

// Assertions
if assert.NoError(t, GetUserHandler(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "User 123")
assert.Contains(t, rec.Body.String(), `"id":"123"`)
}
}

Note how we set the path parameter values using SetParamNames and SetParamValues.

Testing POST Requests with JSON Body

For testing handlers that process JSON request bodies:

go
// handlers.go
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}

type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}

func CreateUserHandler(c echo.Context) error {
u := new(CreateUserRequest)
if err := c.Bind(u); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

// In a real app, you would save to a database and generate a real ID
return c.JSON(http.StatusCreated, User{
ID: "new-user-id",
Name: u.Name,
Email: u.Email,
})
}

Now the test:

go
func TestCreateUserHandler(t *testing.T) {
// Setup
e := echo.New()
jsonBody := `{"name":"John Doe","email":"[email protected]"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(jsonBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Assertions
if assert.NoError(t, CreateUserHandler(c)) {
assert.Equal(t, http.StatusCreated, rec.Code)
assert.Contains(t, rec.Body.String(), "John Doe")
assert.Contains(t, rec.Body.String(), "[email protected]")
assert.Contains(t, rec.Body.String(), "new-user-id")
}
}

Testing Middleware

Echo middleware can also be unit tested. Here's an example of a simple authentication middleware and its test:

go
// middleware.go
func AuthMiddleware(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, "Invalid token")
}
return next(c)
}
}

And its test:

go
func TestAuthMiddleware(t *testing.T) {
// Setup
e := echo.New()

// Test case 1: Valid token
req1 := httptest.NewRequest(http.MethodGet, "/", nil)
req1.Header.Set("Authorization", "valid-token")
rec1 := httptest.NewRecorder()
c1 := e.NewContext(req1, rec1)

// Create a handler to use with the middleware
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "test")
}

// Test valid token
h := AuthMiddleware(handler)
assert.NoError(t, h(c1))
assert.Equal(t, http.StatusOK, rec1.Code)

// Test case 2: Invalid token
req2 := httptest.NewRequest(http.MethodGet, "/", nil)
req2.Header.Set("Authorization", "invalid-token")
rec2 := httptest.NewRecorder()
c2 := e.NewContext(req2, rec2)

h = AuthMiddleware(handler)
err := h(c2)

// Check that it returns the correct error
httpErr, ok := err.(*echo.HTTPError)
assert.True(t, ok)
assert.Equal(t, http.StatusUnauthorized, httpErr.Code)
}

Testing with Database Mocks

In real applications, your handlers will often interact with a database. Here's how to test them using mocks:

go
// user_service.go
type UserService interface {
GetUserByID(id string) (*User, error)
}

// handlers.go
func GetUserDetailHandler(userService UserService) echo.HandlerFunc {
return func(c echo.Context) error {
id := c.Param("id")
user, err := userService.GetUserByID(id)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user")
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
return c.JSON(http.StatusOK, user)
}
}

Let's create a mock for testing:

go
// mocks.go
type MockUserService struct {
GetUserByIDFunc func(id string) (*User, error)
}

func (m *MockUserService) GetUserByID(id string) (*User, error) {
return m.GetUserByIDFunc(id)
}

And now the test:

go
func TestGetUserDetailHandler(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/users/:id")
c.SetParamNames("id")
c.SetParamValues("123")

// Create a mock
mockService := &MockUserService{
GetUserByIDFunc: func(id string) (*User, error) {
return &User{
ID: "123",
Name: "Test User",
Email: "[email protected]",
}, nil
},
}

// Get the handler with injected mock
handler := GetUserDetailHandler(mockService)

// Test
if assert.NoError(t, handler(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"id":"123"`)
assert.Contains(t, rec.Body.String(), "Test User")
}

// Test not found case
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)
c.SetPath("/users/:id")
c.SetParamNames("id")
c.SetParamValues("999")

mockService.GetUserByIDFunc = func(id string) (*User, error) {
return nil, nil // User not found
}

handler = GetUserDetailHandler(mockService)
err := handler(c)
httpErr, ok := err.(*echo.HTTPError)
assert.True(t, ok)
assert.Equal(t, http.StatusNotFound, httpErr.Code)
}

Testing Error Handling

Proper error handling is important in web applications. Here's how to test error scenarios:

go
func TestCreateUserHandler_ValidationError(t *testing.T) {
// Setup with invalid JSON
e := echo.New()
invalidJSON := `{"name":"}` // Incomplete JSON
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(invalidJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Call handler
err := CreateUserHandler(c)

// Check that it returns a 400 Bad Request
httpErr, ok := err.(*echo.HTTPError)
assert.True(t, ok)
assert.Equal(t, http.StatusBadRequest, httpErr.Code)
}

Best Practices for Echo Unit Testing

To ensure your Echo tests are effective:

  1. Test both success and error paths: Don't just test the "happy path"
  2. Use descriptive test names: Test names should describe what is being tested
  3. Mock external dependencies: Database calls, external APIs, etc.
  4. Test all handler logic: Including validation, permission checks, etc.
  5. Use table-driven tests for similar test cases:
go
func TestHealthCheckEndpoint(t *testing.T) {
// Setup
e := echo.New()

tests := []struct {
name string
path string
expectedCode int
expectedBody string
}{
{
name: "Basic health check",
path: "/health",
expectedCode: http.StatusOK,
expectedBody: `{"status":"healthy"}`,
},
{
name: "Detailed health check",
path: "/health/detailed",
expectedCode: http.StatusOK,
expectedBody: `"version"`,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Call your handler based on the path
var err error
if tc.path == "/health" {
err = HealthCheckHandler(c)
} else {
err = DetailedHealthCheckHandler(c)
}

assert.NoError(t, err)
assert.Equal(t, tc.expectedCode, rec.Code)
assert.Contains(t, rec.Body.String(), tc.expectedBody)
})
}
}

Running Your Tests

To run your tests, use the Go test command:

bash
go test ./... -v

For coverage reports:

bash
go test ./... -cover

Or for a detailed coverage report:

bash
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out

Summary

In this guide, you've learned the fundamentals of unit testing Echo applications:

  • Setting up the testing environment
  • Testing basic handlers
  • Testing handlers with path parameters
  • Testing handlers that process JSON requests
  • Testing middleware
  • Mocking database dependencies
  • Testing error scenarios

By applying these techniques, you'll be able to create robust, well-tested Echo applications that inspire confidence and are easier to maintain.

Further Resources

Practice Exercises

  1. Write tests for an Echo handler that processes form data instead of JSON
  2. Create tests for a handler that uses query parameters
  3. Write a test for a complex middleware that checks user roles
  4. Implement tests for file upload functionality
  5. Create a complete test suite for a RESTful CRUD API with all endpoints


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