Skip to main content

Echo Continuous Testing

Introduction

Continuous testing is a development practice that involves running automated tests throughout the software development lifecycle to identify and address issues early. In the context of Echo applications, continuous testing ensures that your APIs, middleware, and handlers function correctly as you develop and deploy your code.

This guide explores how to set up and implement continuous testing for Echo applications, enabling you to catch bugs early, improve code quality, and deploy with confidence.

Understanding Continuous Testing

Continuous testing is more than just running tests; it's about integrating testing into your development workflow to get immediate feedback on code changes. For Echo applications, this means:

  1. Automated testing: Writing tests that run automatically
  2. Fast feedback cycles: Getting results quickly after code changes
  3. Integration with CI/CD: Running tests on every commit or pull request
  4. Comprehensive coverage: Testing different aspects of your application

Setting Up Your Testing Environment

Prerequisites

Before implementing continuous testing, you'll need:

  • Go installed on your development machine
  • An Echo application
  • Basic familiarity with Go testing

Basic Testing Structure

In Go, tests are placed in files with names ending in _test.go. For Echo applications, you might organize your tests like this:

my-echo-app/
├── handlers/
│ ├── user_handler.go
│ ├── user_handler_test.go
│ ├── product_handler.go
│ └── product_handler_test.go
├── middleware/
│ ├── auth.go
│ └── auth_test.go
├── main.go
└── main_test.go

Writing Tests for Echo Applications

Testing HTTP Handlers

Here's a basic example of testing an Echo HTTP handler:

go
// handlers/user_handler_test.go
package handlers

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

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

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

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

Testing Middleware

Middleware testing requires checking if the middleware performs its intended function:

go
// middleware/auth_test.go
package middleware

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

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

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

// Test with missing auth token
h := AuthMiddleware()(func(c echo.Context) error {
return c.String(http.StatusOK, "test")
})

// Assert unauthorized access is blocked
assert.Error(t, h(c))
assert.Equal(t, http.StatusUnauthorized, rec.Code)

// Test with valid token
req.Header.Set("Authorization", "Bearer valid-token")
rec = httptest.NewRecorder()
c = e.NewContext(req, rec)

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

Implementing Continuous Testing

Now that you have tests, let's integrate them into a continuous testing workflow.

Local Development Testing

For fast feedback during development, use these techniques:

1. Using go test with the -watch flag

You can use tools like reflex or air to watch for file changes and run tests automatically:

bash
# Install reflex
go install github.com/cespare/reflex@latest

# Watch for changes and run tests
reflex -r '\.go$' -s -- go test ./...

2. Running a subset of tests

To focus on specific tests during development:

bash
# Run tests in a specific package
go test ./handlers

# Run a specific test
go test -run TestGetUserHandler ./handlers

Continuous Integration Setup

GitHub Actions Example

Create a file at .github/workflows/test.yml:

yaml
name: Echo Continuous Testing

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.19'

- name: Install dependencies
run: go mod download

- name: Run tests
run: go test -v ./...

- name: Run tests with race detection
run: go test -race -v ./...

GitLab CI Example

Create a file at .gitlab-ci.yml:

yaml
stages:
- test

go-test:
stage: test
image: golang:1.19
script:
- go mod download
- go test -v ./...
- go test -race -v ./...

Advanced Testing Techniques

Table-Driven Tests

Table-driven tests allow you to test multiple scenarios efficiently:

go
func TestUserAPI(t *testing.T) {
e := echo.New()

tests := []struct {
name string
route string
method string
body string
expectedStatus int
expectedBody string
}{
{
name: "Get User",
route: "/users/1",
method: http.MethodGet,
expectedStatus: http.StatusOK,
expectedBody: `{"id":1,"name":"John"}`,
},
{
name: "User Not Found",
route: "/users/999",
method: http.MethodGet,
expectedStatus: http.StatusNotFound,
expectedBody: `{"message":"User not found"}`,
},
// Add more test cases
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(test.method, test.route, strings.NewReader(test.body))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, test.expectedStatus, rec.Code)
assert.JSONEq(t, test.expectedBody, rec.Body.String())
})
}
}

Integration Testing with a Test Database

For more comprehensive testing, you can set up a test database:

go
func TestUserCRUD(t *testing.T) {
// Setup test database
db, err := setupTestDB()
if err != nil {
t.Fatalf("Failed to set up test database: %v", err)
}
defer cleanupTestDB(db)

// Initialize Echo with the test database
e := setupEcho(db)

// Test user creation
userJSON := `{"name":"Test User","email":"[email protected]"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(userJSON))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusCreated, rec.Code)

// Extract the created user ID for further tests
var response map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
userID := response["id"]

// Test retrieving the created user
req = httptest.NewRequest(http.MethodGet, "/users/"+userID.(string), nil)
rec = httptest.NewRecorder()
e.ServeHTTP(rec, req)

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

Best Practices for Continuous Testing

  1. Write tests as you code: Don't leave testing as an afterthought
  2. Test edge cases: Include tests for error conditions and boundary scenarios
  3. Keep tests fast: Optimize tests to run quickly for faster feedback loops
  4. Use mocks appropriately: Mock external dependencies for unit tests
  5. Monitor test coverage: Aim for high test coverage but focus on critical paths
  6. Separate unit and integration tests: Allow running quick unit tests during development
  7. Set up automated testing gates: Prevent merging code that fails tests

Real-World Example: E-commerce API Testing

Let's see how continuous testing would work for an Echo-based e-commerce API:

go
// handlers/product_handler_test.go
package handlers

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"

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

func TestProductAPI(t *testing.T) {
// Setup
e := echo.New()
testRepo := repositories.NewTestProductRepository()
handler := NewProductHandler(testRepo)

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

// Seed test data
testRepo.AddProduct(&models.Product{
ID: 1,
Name: "Test Product",
Price: 19.99,
Stock: 100,
})

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

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

var products []models.Product
err := json.Unmarshal(rec.Body.Bytes(), &products)
assert.NoError(t, err)
assert.Len(t, products, 1)
assert.Equal(t, "Test Product", products[0].Name)

// Test creating a product
newProduct := `{"name":"New Product","price":29.99,"stock":50}`
req = httptest.NewRequest(http.MethodPost, "/products", strings.NewReader(newProduct))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec = httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusCreated, rec.Code)

var createdProduct models.Product
err = json.Unmarshal(rec.Body.Bytes(), &createdProduct)
assert.NoError(t, err)
assert.Equal(t, "New Product", createdProduct.Name)
assert.Equal(t, 29.99, createdProduct.Price)

// Test getting a product
req = httptest.NewRequest(http.MethodGet, "/products/1", nil)
rec = httptest.NewRecorder()
e.ServeHTTP(rec, req)

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

var product models.Product
err = json.Unmarshal(rec.Body.Bytes(), &product)
assert.NoError(t, err)
assert.Equal(t, 1, product.ID)
assert.Equal(t, "Test Product", product.Name)
}

Summary

Continuous testing is an essential practice for developing robust Echo applications. By implementing automated tests that run throughout your development workflow, you can:

  • Catch bugs early in the development process
  • Ensure code reliability and correctness
  • Build confidence in code changes
  • Facilitate faster, safer deployments

By combining Echo's testing-friendly design with continuous integration tools, you can create a robust testing pipeline that promotes code quality and helps deliver stable, reliable web applications.

Additional Resources

Exercises

  1. Create a simple Echo API with two endpoints and write tests for both endpoints.
  2. Set up a GitHub Actions workflow that runs your Echo tests on every push.
  3. Implement table-driven tests for validating request parameters in an Echo handler.
  4. Create a mock database repository and use it to test database-dependent handlers.
  5. Add test coverage reporting to your continuous integration pipeline.


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