Skip to main content

Echo Integration Testing

Introduction

Integration testing is a crucial aspect of application development that goes beyond unit testing. While unit tests verify individual components in isolation, integration tests ensure that different parts of your application work together correctly. In the context of Echo, a high-performance web framework for Go, integration testing allows you to validate that your routes, middleware, handlers, and data stores all interact as expected.

In this guide, we'll explore how to set up and perform integration tests for your Echo applications. You'll learn how to create test environments, simulate HTTP requests, validate responses, and ensure your application components integrate properly.

Why Integration Testing Matters

Before diving into the practical aspects, let's understand why integration testing is essential:

  1. Validates component interaction: Ensures that separate components of your system work together correctly
  2. Catches integration bugs: Identifies issues that unit tests might miss
  3. Tests the request-response cycle: Verifies the entire HTTP flow through your Echo application
  4. Builds confidence in deployments: Increases certainty that your application will work in production

Setting Up for Echo Integration Testing

Prerequisites

To follow along with this guide, you should have:

  • Go installed on your machine (version 1.16+)
  • Basic knowledge of Echo framework
  • Understanding of Go testing principles

Required Packages

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

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

The httptest package is particularly important as it allows us to create test servers and simulate HTTP requests without needing an actual network connection.

Creating a Test Echo Instance

The first step in integration testing is to create a test instance of your Echo application:

go
func createTestEcho() *echo.Echo {
e := echo.New()

// Register routes and middleware as in your real application
e.GET("/users", getUsers)
e.POST("/users", createUser)

// Add any middleware that's relevant for testing
e.Use(middleware.Logger())

return e
}

Basic Echo Integration Test Structure

Here's a basic pattern for writing Echo integration tests:

go
func TestUserEndpoints(t *testing.T) {
// Create a test Echo instance
e := createTestEcho()

// Define the request to test
req := httptest.NewRequest(http.MethodGet, "/users", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

// Create a response recorder
rec := httptest.NewRecorder()

// Serve the request to the test instance
e.ServeHTTP(rec, req)

// Assert expectations
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "user_list")
}

Testing Different HTTP Methods

Testing GET Requests

go
func TestGetUsers(t *testing.T) {
e := createTestEcho()

req := httptest.NewRequest(http.MethodGet, "/users", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "users")
}

Testing POST Requests with JSON Body

go
func TestCreateUser(t *testing.T) {
e := createTestEcho()

// Create a JSON payload
jsonBody := `{"name":"John Doe","email":"john@example.com"}`

req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(jsonBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

// Assertions
assert.Equal(t, http.StatusCreated, rec.Code)
assert.Contains(t, rec.Body.String(), "john@example.com")
}

Testing PUT and DELETE Requests

go
func TestUpdateUser(t *testing.T) {
e := createTestEcho()

// Create a JSON payload for update
jsonBody := `{"name":"Updated Name"}`

req := httptest.NewRequest(http.MethodPut, "/users/123", strings.NewReader(jsonBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)
}

func TestDeleteUser(t *testing.T) {
e := createTestEcho()

req := httptest.NewRequest(http.MethodDelete, "/users/123", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusNoContent, rec.Code)
}

Testing with Path and Query Parameters

Path Parameters

go
func TestGetUserById(t *testing.T) {
e := createTestEcho()

req := httptest.NewRequest(http.MethodGet, "/users/42", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"id":"42"`)
}

Query Parameters

go
func TestSearchUsers(t *testing.T) {
e := createTestEcho()

req := httptest.NewRequest(http.MethodGet, "/users?search=john&role=admin", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "john")
}

Testing with Authentication and Headers

Many real-world applications require authentication. Here's how to test endpoints that require authentication:

go
func TestAuthenticatedEndpoint(t *testing.T) {
e := createTestEcho()

req := httptest.NewRequest(http.MethodGet, "/protected-resource", nil)

// Add authentication header
req.Header.Set("Authorization", "Bearer test-token-here")

rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)
}

Real-world Example: Todo API Integration Test

Let's put it all together with a more comprehensive example of integration testing for a simple Todo API:

First, let's define the handlers we're going to test:

go
// Models
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
}

// Handlers
func getAllTodos(c echo.Context) error {
todos := []Todo{
{ID: 1, Title: "Learn Echo", Completed: false},
{ID: 2, Title: "Build REST API", Completed: false},
}
return c.JSON(http.StatusOK, todos)
}

func createTodo(c echo.Context) error {
todo := new(Todo)
if err := c.Bind(todo); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid input"})
}
// In a real app, save to database and get ID
todo.ID = 3
return c.JSON(http.StatusCreated, todo)
}

Now, let's write integration tests for these handlers:

go
func TestTodoIntegration(t *testing.T) {
// Setup
e := echo.New()
e.GET("/todos", getAllTodos)
e.POST("/todos", createTodo)

t.Run("Get all todos", func(t *testing.T) {
// Create request
req := httptest.NewRequest(http.MethodGet, "/todos", nil)
rec := httptest.NewRecorder()

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

// Assertions
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Learn Echo")
assert.Contains(t, rec.Body.String(), "Build REST API")
})

t.Run("Create new todo", func(t *testing.T) {
// Create JSON body
jsonBody := `{"title":"Write tests","completed":false}`

// Create request with body
req := httptest.NewRequest(http.MethodPost, "/todos", strings.NewReader(jsonBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()

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

// Assertions
assert.Equal(t, http.StatusCreated, rec.Code)
assert.Contains(t, rec.Body.String(), "Write tests")
assert.Contains(t, rec.Body.String(), `"id":3`)
})

t.Run("Create todo with invalid input", func(t *testing.T) {
// Create invalid JSON body
jsonBody := `{"title":123}` // Title should be a string

// Create request with body
req := httptest.NewRequest(http.MethodPost, "/todos", strings.NewReader(jsonBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()

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

// Assertions
assert.Equal(t, http.StatusBadRequest, rec.Code)
assert.Contains(t, rec.Body.String(), "Invalid input")
})
}

Testing with Database Integration

For true integration tests, you'll often need to test with an actual database. Here's a pattern using an in-memory SQLite database for tests:

go
func TestWithDatabase(t *testing.T) {
// Setup in-memory database for testing
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
defer db.Close()

// Initialize schema
_, err = db.Exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
`)
if err != nil {
t.Fatalf("Failed to create test table: %v", err)
}

// Create Echo instance with database dependency
e := echo.New()
e.GET("/db-users", func(c echo.Context) error {
rows, err := db.Query("SELECT id, name, email FROM users")
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
defer rows.Close()

users := []map[string]interface{}{}
for rows.Next() {
var id int
var name, email string
rows.Scan(&id, &name, &email)
users = append(users, map[string]interface{}{
"id": id,
"name": name,
"email": email,
})
}

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

// Insert test data
_, err = db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", "Test User", "test@example.com")
if err != nil {
t.Fatalf("Failed to insert test data: %v", err)
}

// Test the endpoint
req := httptest.NewRequest(http.MethodGet, "/db-users", nil)
rec := httptest.NewRecorder()

e.ServeHTTP(rec, req)

// Assertions
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Test User")
assert.Contains(t, rec.Body.String(), "test@example.com")
}

Best Practices for Echo Integration Testing

  1. Isolate tests: Each test should be independent and not rely on the state from other tests.

  2. Use subtests with t.Run(): Group related tests to provide better structure and to allow running specific test cases.

  3. Mock external dependencies: For services like databases or third-party APIs, consider using mocks when appropriate.

  4. Test failure cases: Don't just test the happy path; ensure your error handling works correctly.

  5. Clean up after tests: If your tests create resources (files, database records), make sure to clean them up afterward.

  6. Use table-driven tests for similar test cases with different inputs:

go
func TestEndpointWithMultipleInputs(t *testing.T) {
e := createTestEcho()

testCases := []struct {
name string
method string
path string
body string
expectedStatus int
expectedBody string
}{
{
name: "Valid input",
method: http.MethodPost,
path: "/resource",
body: `{"name":"Valid"}`,
expectedStatus: http.StatusCreated,
expectedBody: "success",
},
{
name: "Invalid input",
method: http.MethodPost,
path: "/resource",
body: `{"name":""}`,
expectedStatus: http.StatusBadRequest,
expectedBody: "error",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)

rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, tc.expectedStatus, rec.Code)
assert.Contains(t, rec.Body.String(), tc.expectedBody)
})
}
}

Summary

Integration testing is an essential part of building reliable Echo applications. In this guide, we've explored:

  1. Setting up Echo integration tests
  2. Testing various HTTP methods (GET, POST, PUT, DELETE)
  3. Working with path and query parameters
  4. Handling authentication and headers in tests
  5. Testing database integration
  6. Best practices for integration testing

By implementing comprehensive integration tests, you ensure that your Echo application works as a cohesive whole. These tests provide confidence that your components integrate correctly and that your API behaves as expected under various conditions.

Additional Resources and Exercises

Resources

Exercises

  1. Basic Integration Test: Create an Echo application with a simple CRUD API for a "Book" resource and write integration tests for all endpoints.

  2. Authentication Test: Implement JWT authentication in your Echo application and write tests to ensure protected routes are accessible only with a valid token.

  3. Database Integration: Connect your Echo application to a database and write integration tests that verify the data flow from HTTP requests to database operations and back.

  4. Error Handling: Add comprehensive error handling to your Echo application and write tests that verify your application responds appropriately to various error conditions.

  5. File Upload: Create an endpoint that handles file uploads and write tests to verify the upload functionality works correctly.

By completing these exercises, you'll gain practical experience in writing integration tests for Echo applications, which will help you build more reliable web services.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)