Skip to main content

Echo HTTP Testing

When building web applications with the Echo framework, testing your HTTP endpoints is crucial to ensure reliability and correctness. Echo provides powerful built-in testing utilities that make it easy to validate your API behavior. This guide walks you through the process of testing HTTP endpoints in Echo applications.

Introduction to Echo HTTP Testing

Echo HTTP testing allows you to simulate HTTP requests to your application without actually starting a server. This approach offers several advantages:

  • Tests run faster as they don't require network communication
  • You can isolate specific parts of your application for targeted testing
  • Tests can be automated as part of your CI/CD pipeline
  • You can verify both request handling and response generation

By the end of this guide, you'll understand how to write effective HTTP tests for your Echo applications.

Setting Up Echo for Testing

Before writing tests, you need to set up your testing environment. Here's how to create a testable Echo instance:

go
package main

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

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

// Now you can register your handlers and run tests
}

Basic HTTP Testing Pattern

The basic pattern for testing an Echo HTTP handler follows these steps:

  1. Create a new Echo instance
  2. Register the handler you want to test
  3. Create a request with the desired method, URL, and body
  4. Record the response using an HTTP response recorder
  5. Assert that the response matches your expectations

Here's a complete example testing a simple greeting endpoint:

go
func TestGreetingHandler(t *testing.T) {
// Setup
e := echo.New()
e.GET("/hello/:name", func(c echo.Context) error {
name := c.Param("name")
return c.String(http.StatusOK, "Hello, "+name+"!")
})

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

// Perform the request
e.ServeHTTP(rec, req)

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

expected := "Hello, John!"
if rec.Body.String() != expected {
t.Errorf("Expected body %q but got %q", expected, rec.Body.String())
}
}

Testing Different HTTP Methods

Echo supports testing various HTTP methods. Here's how to test POST, PUT, and DELETE:

Testing POST Requests

go
func TestPostHandler(t *testing.T) {
// Setup
e := echo.New()
e.POST("/users", func(c echo.Context) error {
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
u := new(User)
if err := c.Bind(u); err != nil {
return err
}
return c.JSON(http.StatusCreated, u)
})

// Create a JSON request body
userJSON := `{"name":"Alice","email":"[email protected]"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(userJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()

// Perform the request
e.ServeHTTP(rec, req)

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

expected := `{"name":"Alice","email":"[email protected]"}`
// Trim newlines for comparison
actual := strings.TrimSpace(rec.Body.String())
if actual != expected {
t.Errorf("Expected body %q but got %q", expected, actual)
}
}

Testing PUT Requests

go
func TestPutHandler(t *testing.T) {
// Setup
e := echo.New()
e.PUT("/users/:id", func(c echo.Context) error {
id := c.Param("id")
type User struct {
ID string `json:"id"`
Name string `json:"name"`
}
u := new(User)
if err := c.Bind(u); err != nil {
return err
}
u.ID = id
return c.JSON(http.StatusOK, u)
})

// Create a JSON request body
userJSON := `{"name":"Updated Name"}`
req := httptest.NewRequest(http.MethodPut, "/users/123", strings.NewReader(userJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()

// Perform the request
e.ServeHTTP(rec, req)

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

expected := `{"id":"123","name":"Updated Name"}`
actual := strings.TrimSpace(rec.Body.String())
if actual != expected {
t.Errorf("Expected body %q but got %q", expected, actual)
}
}

Testing Request Headers and Cookies

Sometimes you need to test endpoints that require specific headers or cookies. Here's how to test them:

go
func TestHeadersAndCookies(t *testing.T) {
// Setup
e := echo.New()
e.GET("/protected", func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token != "Bearer valid-token" {
return c.String(http.StatusUnauthorized, "Unauthorized")
}

cookie, err := c.Cookie("session")
if err != nil || cookie.Value != "abc123" {
return c.String(http.StatusBadRequest, "Invalid session")
}

return c.String(http.StatusOK, "Access granted")
})

// Create a request with headers and cookies
req := httptest.NewRequest(http.MethodGet, "/protected", nil)
req.Header.Set("Authorization", "Bearer valid-token")

cookie := &http.Cookie{
Name: "session",
Value: "abc123",
}
req.AddCookie(cookie)

rec := httptest.NewRecorder()

// Perform the request
e.ServeHTTP(rec, req)

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

expected := "Access granted"
if rec.Body.String() != expected {
t.Errorf("Expected body %q but got %q", expected, rec.Body.String())
}
}

Testing File Uploads

Testing file uploads requires simulating a multipart form request:

go
func TestFileUpload(t *testing.T) {
// Setup
e := echo.New()
e.POST("/upload", func(c echo.Context) error {
// Get file
file, err := c.FormFile("file")
if err != nil {
return err
}

// Just return the file name for testing
return c.String(http.StatusOK, "Uploaded: "+file.Filename)
})

// Create a multipart form request
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)

// Create a form file field
part, err := writer.CreateFormFile("file", "test.txt")
if err != nil {
t.Fatal(err)
}

// Write content to the form file
part.Write([]byte("file content for testing"))
writer.Close()

// Create request
req := httptest.NewRequest(http.MethodPost, "/upload", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
rec := httptest.NewRecorder()

// Perform the request
e.ServeHTTP(rec, req)

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

expected := "Uploaded: test.txt"
if rec.Body.String() != expected {
t.Errorf("Expected body %q but got %q", expected, rec.Body.String())
}
}

Testing Error Responses

It's important to test error handling in your API:

go
func TestErrorResponse(t *testing.T) {
// Setup
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
if id == "999" {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "User not found",
})
}
return c.String(http.StatusOK, "User found")
})

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

// Perform the request
e.ServeHTTP(rec, req)

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

expected := `{"error":"User not found"}`
actual := strings.TrimSpace(rec.Body.String())
if actual != expected {
t.Errorf("Expected body %q but got %q", expected, actual)
}
}

Testing Middleware

Echo middleware can be tested by including it in your Echo instance:

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

// Add a simple logging middleware
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("X-Request-ID", "test-id")
return next(c)
}
})

// Add a route
e.GET("/test", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})

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

// Perform the request
e.ServeHTTP(rec, req)

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

// Verify middleware added the header
requestID := rec.Header().Get("X-Request-ID")
if requestID != "test-id" {
t.Errorf("Expected X-Request-ID header to be %q but got %q", "test-id", requestID)
}

expected := "Hello, World!"
if rec.Body.String() != expected {
t.Errorf("Expected body %q but got %q", expected, rec.Body.String())
}
}

Real-World Example: Testing a RESTful API

Let's put everything together in a more complex example of testing a RESTful API that manages a product catalog:

go
package main

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

// Product represents a product in our catalog
type Product struct {
ID string `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}

// ProductHandler contains handlers for product-related endpoints
type ProductHandler struct {
products map[string]Product
}

// NewProductHandler creates a new product handler
func NewProductHandler() *ProductHandler {
// Initialize with some sample data
products := map[string]Product{
"1": {ID: "1", Name: "Laptop", Price: 999.99},
"2": {ID: "2", Name: "Mouse", Price: 24.99},
}

return &ProductHandler{products: products}
}

// GetProduct returns a product by ID
func (h *ProductHandler) GetProduct(c echo.Context) error {
id := c.Param("id")
product, exists := h.products[id]
if !exists {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "Product not found",
})
}

return c.JSON(http.StatusOK, product)
}

// CreateProduct creates a new product
func (h *ProductHandler) CreateProduct(c echo.Context) error {
p := new(Product)
if err := c.Bind(p); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid product data",
})
}

// Generate a simple ID (in production, use UUID)
p.ID = "3" // For demonstration purposes

h.products[p.ID] = *p
return c.JSON(http.StatusCreated, p)
}

// TestProductAPI tests the product API endpoints
func TestProductAPI(t *testing.T) {
// Setup
e := echo.New()
handler := NewProductHandler()

// Register routes
e.GET("/products/:id", handler.GetProduct)
e.POST("/products", handler.CreateProduct)

// Test case 1: Get existing product
t.Run("GetExistingProduct", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/products/1", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

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

var product Product
if err := json.Unmarshal(rec.Body.Bytes(), &product); err != nil {
t.Errorf("Failed to unmarshal response: %v", err)
}

if product.ID != "1" || product.Name != "Laptop" || product.Price != 999.99 {
t.Errorf("Incorrect product data: %+v", product)
}
})

// Test case 2: Get non-existing product
t.Run("GetNonExistingProduct", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/products/999", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

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

// Test case 3: Create new product
t.Run("CreateProduct", func(t *testing.T) {
productJSON := `{"name":"Keyboard","price":49.99}`
req := httptest.NewRequest(http.MethodPost, "/products", strings.NewReader(productJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

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

var product Product
if err := json.Unmarshal(rec.Body.Bytes(), &product); err != nil {
t.Errorf("Failed to unmarshal response: %v", err)
}

if product.Name != "Keyboard" || product.Price != 49.99 {
t.Errorf("Incorrect product data: %+v", product)
}

// Verify the product was added to the handler's map
if _, exists := handler.products[product.ID]; !exists {
t.Error("Product was not added to the products map")
}
})
}

Best Practices for Echo HTTP Testing

To write effective HTTP tests for your Echo applications, follow these best practices:

  1. Test the right thing: Focus on testing the behavior, not the implementation details.

  2. Use table-driven tests: For similar endpoints with different inputs, use table-driven tests to reduce code duplication.

  3. Separate test setup: Extract common setup code into helper functions.

  4. Test edge cases: Test invalid inputs, boundary conditions, and error scenarios.

  5. Mock external dependencies: Use interfaces and mock implementations for database access, external APIs, etc.

  6. Test middleware: Ensure your middleware functions correctly in isolation and when applied to routes.

  7. Use subtests: Organize related tests with t.Run() to improve readability and isolation.

  8. Test response headers and cookies: Don't forget to test these important parts of HTTP responses.

  9. Keep tests independent: Each test should work independently without relying on the state from other tests.

  10. Clean up after tests: If your tests create resources, make sure to clean them up afterward.

Summary

Echo's HTTP testing utilities provide a powerful way to verify your web API's behavior without starting a full server. By simulating HTTP requests, you can test your handlers' responses to different inputs, validate the correct handling of various HTTP methods, and ensure your middleware works as expected.

In this guide, you've learned:

  • How to set up Echo for testing
  • Testing different HTTP methods (GET, POST, PUT, DELETE)
  • How to test header and cookie handling
  • Testing file uploads
  • Validating error responses
  • Testing middleware functionality
  • A real-world example of testing a RESTful API

By incorporating HTTP testing into your development workflow, you'll catch bugs earlier, ensure your API behaves consistently, and build more reliable web applications.

Additional Resources

Exercises

  1. Write tests for an Echo API that implements CRUD operations for a "Todo" resource.
  2. Create tests for an authentication middleware that verifies JWT tokens.
  3. Implement tests for an API that returns different responses based on query parameters.
  4. Write a test that validates a file upload endpoint limits file sizes to 5MB.
  5. Create tests for an API that returns paginated results with next/previous page links.


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