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:
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:
- Creating an Echo instance
- Registering the middleware to be tested
- Defining a simple handler
- Creating a test request
- Recording the response using
httptest.ResponseRecorder
- 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:
// 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:
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:
// 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:
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:
// 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:
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:
// 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:
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:
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:
// 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:
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
- Isolate dependencies: Mock external dependencies like databases or external APIs when testing middleware.
- Test error scenarios: Make sure to test failure cases, not just the happy path.
- Use table-driven tests: For middleware with multiple behavior variations, use table-driven tests to cover all cases.
- Test middleware chains: Test middleware in isolation and also in combination with other middleware.
- 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:
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
- Create and test a middleware that logs the request processing time.
- Implement and test a middleware that validates request bodies against a schema.
- Write tests for a CORS middleware with different configuration settings.
- Create a middleware that handles request timeouts and write comprehensive tests for it.
- 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! :)