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:
- Automated testing: Writing tests that run automatically
- Fast feedback cycles: Getting results quickly after code changes
- Integration with CI/CD: Running tests on every commit or pull request
- 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:
// 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:
// 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:
# 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:
# 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
:
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
:
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:
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:
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
- Write tests as you code: Don't leave testing as an afterthought
- Test edge cases: Include tests for error conditions and boundary scenarios
- Keep tests fast: Optimize tests to run quickly for faster feedback loops
- Use mocks appropriately: Mock external dependencies for unit tests
- Monitor test coverage: Aim for high test coverage but focus on critical paths
- Separate unit and integration tests: Allow running quick unit tests during development
- 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:
// 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
- Echo Framework Official Testing Guide
- Go Testing Documentation
- GitHub Actions Documentation
- Testify Package Documentation
- The Go Blog: Using Subtests
Exercises
- Create a simple Echo API with two endpoints and write tests for both endpoints.
- Set up a GitHub Actions workflow that runs your Echo tests on every push.
- Implement table-driven tests for validating request parameters in an Echo handler.
- Create a mock database repository and use it to test database-dependent handlers.
- 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! :)