Skip to main content

Echo Middleware Testing

Middleware functions play a crucial role in Echo applications by processing requests before they reach route handlers or modifying responses afterward. Testing these middleware components thoroughly is essential for building reliable web applications. This guide will walk you through the process of testing middleware in Echo applications.

Introduction to Middleware Testing

In the Echo framework, middleware are functions that have access to the request and response objects, as well as the next middleware in the chain. They can perform operations before and after the next middleware or handler is called, making them perfect for cross-cutting concerns like:

  • Authentication and authorization
  • Request logging
  • CORS handling
  • Response compression
  • Error handling

Testing middleware helps ensure these critical components work correctly in isolation before integrating them into your application.

Setting Up Your Testing Environment

Before diving into testing middleware, let's set up a proper testing environment. You'll need the following imports:

go
import (
"github.com/labstack/echo/v4"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

Basic Middleware Testing Pattern

The general pattern for testing Echo middleware involves:

  1. Creating an Echo instance
  2. Registering the middleware to be tested
  3. Defining a simple handler
  4. Creating a test request
  5. Recording the response using httptest.ResponseRecorder
  6. Asserting the expected behavior

Let's see this pattern in action:

Example: Testing a Logger Middleware

Suppose we have a simple logging middleware that logs request information:

go
// LoggerMiddleware logs request information
func LoggerMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Log request info (simplified for example)
req := c.Request()
logMessage := fmt.Sprintf("Request: %s %s", req.Method, req.URL.Path)

// For testing purposes, store this in the context
c.Set("logMessage", logMessage)

// Call the next handler
return next(c)
}
}

Here's how to test it:

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

// Define test handler
handler := func(c echo.Context) error {
// Access the log message set by the middleware
msg := c.Get("logMessage").(string)
return c.String(http.StatusOK, msg)
}

// Wrap the handler with the middleware
middlewareFunc := LoggerMiddleware(handler)

// Execute request
if err := middlewareFunc(c); err != nil {
t.Errorf("Middleware returned an error: %v", err)
}

// Assertions
expectedLogMsg := "Request: GET /"
if rec.Body.String() != expectedLogMsg {
t.Errorf("Expected log message '%s', got '%s'", expectedLogMsg, rec.Body.String())
}
}

Testing Middleware That Modifies the Request

Let's test middleware that adds a custom header to the request:

go
// HeaderMiddleware adds a custom header to the request
func HeaderMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Request().Header.Set("X-Custom-Header", "TestValue")
return next(c)
}
}

And the test:

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

// Define test handler
handler := func(c echo.Context) error {
// Check if the header was added
headerValue := c.Request().Header.Get("X-Custom-Header")
return c.String(http.StatusOK, headerValue)
}

// Wrap the handler with the middleware
middlewareFunc := HeaderMiddleware(handler)

// Execute request
if err := middlewareFunc(c); err != nil {
t.Errorf("Middleware returned an error: %v", err)
}

// Assertions
expectedValue := "TestValue"
if rec.Body.String() != expectedValue {
t.Errorf("Expected header value '%s', got '%s'", expectedValue, rec.Body.String())
}
}

Testing Middleware That Modifies the Response

Some middleware functions modify the response. Here's an example of middleware that adds a security header to the response:

go
// SecurityHeadersMiddleware adds security headers to the response
func SecurityHeadersMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Process the request
err := next(c)

// Add security header to the response
c.Response().Header().Set("X-XSS-Protection", "1; mode=block")
return err
}
}

And the test:

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

// Define test handler
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "Success")
}

// Wrap the handler with the middleware
middlewareFunc := SecurityHeadersMiddleware(handler)

// Execute request
if err := middlewareFunc(c); err != nil {
t.Errorf("Middleware returned an error: %v", err)
}

// Assertions
expectedHeaderValue := "1; mode=block"
actualHeaderValue := rec.Header().Get("X-XSS-Protection")
if actualHeaderValue != expectedHeaderValue {
t.Errorf("Expected header value '%s', got '%s'", expectedHeaderValue, actualHeaderValue)
}
}

Testing Authentication Middleware

Authentication middleware is common in web applications. Here's an example of a simple token-based authentication middleware:

go
// AuthMiddleware checks for a valid token in the Authorization header
func AuthMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")

// Check if token is valid (simplified example)
if token != "Bearer valid-token" {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or missing token")
}

// Token is valid, proceed to the next handler
return next(c)
}
}

Let's write tests for both valid and invalid token scenarios:

go
func TestAuthMiddleware_ValidToken(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer valid-token")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Define test handler
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "Authenticated")
}

// Wrap the handler with the middleware
middlewareFunc := AuthMiddleware(handler)

// Execute request
err := middlewareFunc(c)

// Assertions
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

if rec.Code != http.StatusOK {
t.Errorf("Expected status code %d, got %d", http.StatusOK, rec.Code)
}

if rec.Body.String() != "Authenticated" {
t.Errorf("Expected body 'Authenticated', got '%s'", rec.Body.String())
}
}

func TestAuthMiddleware_InvalidToken(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Define test handler
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "Authenticated")
}

// Wrap the handler with the middleware
middlewareFunc := AuthMiddleware(handler)

// Execute request
err := middlewareFunc(c)

// Assertions
if err == nil {
t.Error("Expected an error, got nil")
return
}

// Check if it's the correct HTTP error
httpError, ok := err.(*echo.HTTPError)
if !ok {
t.Errorf("Expected *echo.HTTPError, got %T", err)
return
}

if httpError.Code != http.StatusUnauthorized {
t.Errorf("Expected status code %d, got %d", http.StatusUnauthorized, httpError.Code)
}
}

Testing Middleware Chain

In real applications, you often have multiple middleware functions working together. Let's test a middleware chain:

go
func TestMiddlewareChain(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", "Bearer valid-token")
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Define test handler
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "Success")
}

// Create middleware chain
// First apply security headers, then authenticate
chainedHandler := SecurityHeadersMiddleware(AuthMiddleware(handler))

// Execute request
err := chainedHandler(c)

// Assertions
if err != nil {
t.Errorf("Middleware chain returned an error: %v", err)
}

// Check security headers
expectedHeaderValue := "1; mode=block"
actualHeaderValue := rec.Header().Get("X-XSS-Protection")
if actualHeaderValue != expectedHeaderValue {
t.Errorf("Expected header value '%s', got '%s'", expectedHeaderValue, actualHeaderValue)
}

// Check response body
if rec.Body.String() != "Success" {
t.Errorf("Expected body 'Success', got '%s'", rec.Body.String())
}
}

Real-world Example: Rate Limiting Middleware

Let's test a more complex middleware that implements rate limiting:

go
// Simple in-memory store for rate limiting
var requestCounts = make(map[string]int)

// RateLimitMiddleware limits requests to 5 per IP
func RateLimitMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ip := c.RealIP()

// Increment request count
requestCounts[ip]++

// Check if limit exceeded
if requestCounts[ip] > 5 {
return echo.NewHTTPError(http.StatusTooManyRequests, "Rate limit exceeded")
}

// Add headers about rate limiting
c.Response().Header().Set("X-RateLimit-Limit", "5")
c.Response().Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", 5-requestCounts[ip]))

return next(c)
}
}

Now let's test it:

go
func TestRateLimitMiddleware(t *testing.T) {
// Reset the counter before test
requestCounts = make(map[string]int)

// Setup
e := echo.New()
handler := func(c echo.Context) error {
return c.String(http.StatusOK, "Success")
}

middlewareFunc := RateLimitMiddleware(handler)

// Test multiple requests from the same IP
for i := 1; i <= 6; i++ {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

// Execute request
err := middlewareFunc(c)

if i <= 5 {
// First 5 requests should succeed
if err != nil {
t.Errorf("Request %d: Expected no error, got %v", i, err)
}

// Check rate limit headers
remaining := 5 - i
expectedRemaining := fmt.Sprintf("%d", remaining)
actualRemaining := rec.Header().Get("X-RateLimit-Remaining")
if actualRemaining != expectedRemaining {
t.Errorf("Request %d: Expected remaining %s, got %s", i, expectedRemaining, actualRemaining)
}
} else {
// 6th request should be limited
if err == nil {
t.Errorf("Request %d: Expected rate limit error, got nil", i)
continue
}

httpError, ok := err.(*echo.HTTPError)
if !ok {
t.Errorf("Expected *echo.HTTPError, got %T", err)
continue
}

if httpError.Code != http.StatusTooManyRequests {
t.Errorf("Expected status code %d, got %d", http.StatusTooManyRequests, httpError.Code)
}
}
}
}

Testing Tips for Echo Middleware

  1. Isolate dependencies: Mock external dependencies like databases or external APIs when testing middleware.
  2. Test error scenarios: Make sure to test failure cases, not just the happy path.
  3. Use table-driven tests: For middleware with multiple behavior variations, use table-driven tests to cover all cases.
  4. Test middleware chains: Test middleware in isolation and also in combination with other middleware.
  5. Check both request and response: Verify modifications to both incoming requests and outgoing responses.

Common Testing Patterns

Using Table-Driven Tests

Table-driven tests are especially useful for middleware that behaves differently based on inputs:

go
func TestAuthMiddleware_TableDriven(t *testing.T) {
tests := []struct {
name string
token string
expectedError bool
statusCode int
}{
{"Valid token", "Bearer valid-token", false, http.StatusOK},
{"Invalid token", "Bearer invalid-token", true, http.StatusUnauthorized},
{"Missing token", "", true, http.StatusUnauthorized},
{"Malformed token", "Malformed", true, http.StatusUnauthorized},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
if tt.token != "" {
req.Header.Set("Authorization", tt.token)
}
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

handler := func(c echo.Context) error {
return c.String(http.StatusOK, "Authenticated")
}

middlewareFunc := AuthMiddleware(handler)

// Execute request
err := middlewareFunc(c)

// Assertions
if tt.expectedError && err == nil {
t.Error("Expected an error, got nil")
return
}

if !tt.expectedError && err != nil {
t.Errorf("Expected no error, got: %v", err)
return
}

// Further assertions on error type and code
if tt.expectedError {
httpError, ok := err.(*echo.HTTPError)
if !ok {
t.Errorf("Expected *echo.HTTPError, got %T", err)
return
}

if httpError.Code != tt.statusCode {
t.Errorf("Expected status code %d, got %d", tt.statusCode, httpError.Code)
}
}
})
}
}

Summary

Testing middleware in Echo applications is essential to ensure your application behaves correctly. In this guide, you've learned:

  • The basic pattern for testing Echo middleware
  • How to test middleware that modifies requests
  • How to test middleware that modifies responses
  • Testing authentication middleware with valid and invalid scenarios
  • Testing middleware chains
  • A real-world example with rate limiting
  • Best practices and patterns for middleware testing

By thoroughly testing middleware components, you'll catch issues early and build more reliable Echo applications.

Additional Resources

Exercises

  1. Create and test a middleware that logs the request processing time.
  2. Implement and test a middleware that validates request bodies against a schema.
  3. Write tests for a CORS middleware with different configuration settings.
  4. Create a middleware that handles request timeouts and write comprehensive tests for it.
  5. Implement a middleware chain with at least three different middleware functions and write tests that verify they all work together correctly.


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