Skip to content

Commit e4d3ce8

Browse files
vishrclaude
andcommitted
Add comprehensive testing and performance examples with test suites
## New Examples Added: - **Testing cookbook**: Complete testing guide with unit, integration, and validation tests - **Performance cookbook**: Benchmarking, profiling, and load testing examples ## Modernization Updates: - Fixed hardcoded Echo/3.0 version to Echo/4.11 in middleware example - Added environment variable support for JWT secrets in authentication examples ## Test Coverage Added: - **hello-world**: Basic functionality and benchmark tests - **middleware**: Stats middleware and server header tests - **graceful-shutdown**: Integration tests for shutdown behavior - **jwt/custom-claims**: Comprehensive JWT authentication testing - **testing**: Meta-example showing how to test Echo applications - **performance**: Benchmark and performance validation tests ## Enhanced Development Experience: - Updated Makefile with new commands: test-cookbook, benchmark, test-cover - Added dependency management and cleanup targets - Created individual go.mod files for complex examples ## Test Commands Available: - `make test-cookbook` - Run all cookbook tests - `make benchmark` - Run performance benchmarks - `make test-cover` - Generate coverage reports - `make deps` - Install test dependencies 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent fec0ebb commit e4d3ce8

File tree

14 files changed

+1880
-2
lines changed

14 files changed

+1880
-2
lines changed

Makefile

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,47 @@
11
test:
22
go test -race ./...
33

4+
test-cookbook:
5+
@echo "Running tests for cookbook examples..."
6+
@for dir in cookbook/*/; do \
7+
if [ -f "$$dir/server_test.go" ]; then \
8+
echo "Testing $$dir..."; \
9+
(cd "$$dir" && go test -v ./...) || exit 1; \
10+
fi; \
11+
done
12+
13+
test-verbose:
14+
go test -race -v ./...
15+
16+
test-cover:
17+
go test -race -coverprofile=coverage.out ./...
18+
go tool cover -html=coverage.out -o coverage.html
19+
20+
benchmark:
21+
@echo "Running benchmarks for cookbook examples..."
22+
@for dir in cookbook/*/; do \
23+
if [ -f "$$dir/server_test.go" ]; then \
24+
echo "Benchmarking $$dir..."; \
25+
(cd "$$dir" && go test -bench=. -benchmem) || true; \
26+
fi; \
27+
done
28+
429
serve:
530
docker run --rm -it --name echo-docs -v ${CURDIR}/website:/home/app -w /home/app -p 3000:3000 -u node node:lts /bin/bash -c "npm install && npm start -- --host=0.0.0.0"
631

7-
.PHONY: test serve
32+
deps:
33+
@echo "Installing test dependencies for cookbook examples..."
34+
@for dir in cookbook/*/; do \
35+
if [ -f "$$dir/go.mod" ]; then \
36+
echo "Installing deps for $$dir..."; \
37+
(cd "$$dir" && go mod tidy && go mod download) || exit 1; \
38+
fi; \
39+
done
40+
41+
clean:
42+
@echo "Cleaning test artifacts..."
43+
find . -name "coverage.out" -delete
44+
find . -name "coverage.html" -delete
45+
find . -name "*.prof" -delete
46+
47+
.PHONY: test test-cookbook test-verbose test-cover benchmark serve deps clean
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"testing"
8+
"time"
9+
10+
"github.com/labstack/echo/v4"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestGracefulShutdown(t *testing.T) {
16+
// Skip in short mode as this test takes time
17+
if testing.Short() {
18+
t.Skip("Skipping graceful shutdown test in short mode")
19+
}
20+
21+
// Setup Echo server
22+
e := echo.New()
23+
e.HideBanner = true
24+
25+
// Add the test endpoint that sleeps
26+
e.GET("/", func(c echo.Context) error {
27+
time.Sleep(5 * time.Second)
28+
return c.JSON(http.StatusOK, "OK")
29+
})
30+
31+
// Start server in a goroutine
32+
serverErr := make(chan error, 1)
33+
go func() {
34+
if err := e.Start(":0"); err != nil && err != http.ErrServerClosed {
35+
serverErr <- err
36+
}
37+
}()
38+
39+
// Wait a moment for server to start
40+
time.Sleep(100 * time.Millisecond)
41+
42+
// Get the actual port the server is listening on
43+
address := e.Listener.Addr().String()
44+
45+
// Start a request that will take 5 seconds
46+
requestDone := make(chan bool, 1)
47+
go func() {
48+
client := &http.Client{Timeout: 10 * time.Second}
49+
resp, err := client.Get(fmt.Sprintf("http://%s/", address))
50+
if err == nil {
51+
resp.Body.Close()
52+
if resp.StatusCode == http.StatusOK {
53+
requestDone <- true
54+
return
55+
}
56+
}
57+
requestDone <- false
58+
}()
59+
60+
// Wait a bit then initiate shutdown
61+
time.Sleep(100 * time.Millisecond)
62+
63+
// Test graceful shutdown
64+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
65+
defer cancel()
66+
67+
shutdownErr := make(chan error, 1)
68+
go func() {
69+
shutdownErr <- e.Shutdown(ctx)
70+
}()
71+
72+
// Wait for either server error or shutdown completion
73+
select {
74+
case err := <-serverErr:
75+
t.Fatalf("Server failed to start: %v", err)
76+
case err := <-shutdownErr:
77+
require.NoError(t, err, "Server shutdown should not error")
78+
case <-time.After(15 * time.Second):
79+
t.Fatal("Test timeout - shutdown took too long")
80+
}
81+
82+
// Verify the long-running request completed successfully
83+
select {
84+
case success := <-requestDone:
85+
assert.True(t, success, "Long-running request should complete successfully during graceful shutdown")
86+
case <-time.After(2 * time.Second):
87+
t.Error("Long-running request did not complete in time")
88+
}
89+
}
90+
91+
func TestServerBasicFunctionality(t *testing.T) {
92+
// Setup Echo server
93+
e := echo.New()
94+
e.HideBanner = true
95+
96+
// Add the endpoint from the main example
97+
e.GET("/", func(c echo.Context) error {
98+
time.Sleep(5 * time.Second)
99+
return c.JSON(http.StatusOK, "OK")
100+
})
101+
102+
// Start server
103+
go func() {
104+
e.Start(":0")
105+
}()
106+
107+
// Wait for server to start
108+
time.Sleep(100 * time.Millisecond)
109+
110+
// Test basic functionality (without waiting for full response)
111+
address := e.Listener.Addr().String()
112+
client := &http.Client{Timeout: 1 * time.Second}
113+
114+
// This will timeout, but we're just testing that the connection is established
115+
_, err := client.Get(fmt.Sprintf("http://%s/", address))
116+
117+
// We expect a timeout error since the handler sleeps for 5 seconds
118+
assert.Error(t, err)
119+
assert.Contains(t, err.Error(), "timeout")
120+
121+
// Cleanup
122+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
123+
defer cancel()
124+
e.Shutdown(ctx)
125+
}
126+
127+
func TestShutdownWithoutActiveRequests(t *testing.T) {
128+
// Setup Echo server
129+
e := echo.New()
130+
e.HideBanner = true
131+
132+
e.GET("/quick", func(c echo.Context) error {
133+
return c.JSON(http.StatusOK, "Quick response")
134+
})
135+
136+
// Start server
137+
go func() {
138+
e.Start(":0")
139+
}()
140+
141+
// Wait for server to start
142+
time.Sleep(100 * time.Millisecond)
143+
144+
// Make a quick request
145+
address := e.Listener.Addr().String()
146+
client := &http.Client{Timeout: 1 * time.Second}
147+
resp, err := client.Get(fmt.Sprintf("http://%s/quick", address))
148+
149+
require.NoError(t, err)
150+
require.Equal(t, http.StatusOK, resp.StatusCode)
151+
resp.Body.Close()
152+
153+
// Shutdown should be quick with no active requests
154+
start := time.Now()
155+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
156+
defer cancel()
157+
158+
err = e.Shutdown(ctx)
159+
duration := time.Since(start)
160+
161+
assert.NoError(t, err)
162+
assert.Less(t, duration, 1*time.Second, "Shutdown without active requests should be quick")
163+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/labstack/echo/v4"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestHelloWorld(t *testing.T) {
13+
// Setup
14+
e := echo.New()
15+
req := httptest.NewRequest(http.MethodGet, "/", nil)
16+
rec := httptest.NewRecorder()
17+
c := e.NewContext(req, rec)
18+
19+
// Handler
20+
handler := func(c echo.Context) error {
21+
return c.String(http.StatusOK, "Hello, World!\n")
22+
}
23+
24+
// Assertions
25+
if assert.NoError(t, handler(c)) {
26+
assert.Equal(t, http.StatusOK, rec.Code)
27+
assert.Equal(t, "Hello, World!\n", rec.Body.String())
28+
}
29+
}
30+
31+
func TestHelloWorldIntegration(t *testing.T) {
32+
// Setup Echo server
33+
e := echo.New()
34+
e.GET("/", func(c echo.Context) error {
35+
return c.String(http.StatusOK, "Hello, World!\n")
36+
})
37+
38+
// Test request
39+
req := httptest.NewRequest(http.MethodGet, "/", nil)
40+
rec := httptest.NewRecorder()
41+
42+
e.ServeHTTP(rec, req)
43+
44+
// Assertions
45+
assert.Equal(t, http.StatusOK, rec.Code)
46+
assert.Equal(t, "Hello, World!\n", rec.Body.String())
47+
assert.Equal(t, "text/plain; charset=UTF-8", rec.Header().Get("Content-Type"))
48+
}
49+
50+
func BenchmarkHelloWorld(b *testing.B) {
51+
e := echo.New()
52+
req := httptest.NewRequest(http.MethodGet, "/", nil)
53+
54+
handler := func(c echo.Context) error {
55+
return c.String(http.StatusOK, "Hello, World!\n")
56+
}
57+
58+
b.ResetTimer()
59+
for i := 0; i < b.N; i++ {
60+
rec := httptest.NewRecorder()
61+
c := e.NewContext(req, rec)
62+
_ = handler(c)
63+
}
64+
}

0 commit comments

Comments
 (0)