This episode tests the Echo application: unit testing handlers with echo.New and httptest, testing middleware directly, mocking services with interfaces, and testing.B benchmarks to measure allocation and route latency.

One of Echo's biggest advantages is how easy it is to test. Because it stands on top of net/http, all of Go's testing tooling works without adaptation. The layered architecture from episode 8 also lets each layer be tested separately.
Episode 17 tests the Echo application: unit testing handlers with echo.New() and httptest, testing middleware, mocking services with interfaces, and benchmarking with testing.B to measure allocation and latency.
Echo handlers are tested by creating an Echo instance, registering a route, and sending a fake request through httptest:
func TestHello(t *testing.T) {
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "hello")
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, ingin %d", rec.Code, http.StatusOK)
}
if rec.Body.String() != "hello" {
t.Fatalf("body = %q, ingin %q", rec.Body.String(), "hello")
}
}httptest.NewRequest creates a fake request, httptest.NewRecorder captures the response. e.ServeHTTP(rec, req) runs the entire pipeline — router, middleware, handler — exactly like a real request.
For handlers that receive payloads, attach a body and headers:
func TestCreateUser(t *testing.T) {
e := echo.New()
e.Validator = &CustomValidator{validator: validator.New()}
e.POST("/users", createUserHandler)
body := strings.NewReader(`{"name":"Arman","email":"a@dev.id"}`)
req := httptest.NewRequest(http.MethodPost, "/users", body)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("status = %d, ingin %d", rec.Code, http.StatusCreated)
}
}Notice that the validator must also be registered on the test instance — the test instance is a full instance, not a slice.
Middleware is tested directly with a fake context:
func TestSecurityHeaders(t *testing.T) {
e := echo.New()
handler := securityHeaders()(func(c echo.Context) error {
return c.String(http.StatusOK, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
if err := handler(c); err != nil {
t.Fatalf("handler gagal: %v", err)
}
if rec.Header().Get("X-Frame-Options") != "DENY" {
t.Fatal("X-Frame-Options tidak terpasang")
}
}e.NewContext creates an Echo context from a fake request and recorder — a direct way to test a single middleware without a running server.
The episode 8 architecture makes mocking simple: the service accepts a UserRepository interface, and the test simply provides a stub implementation:
type mockRepo struct{}
func (m *mockRepo) FindByID(ctx context.Context, id int) (*model.User, error) {
if id != 42 {
return nil, errors.New("user tidak ditemukan")
}
return &model.User{ID: 42, Name: "Arman"}, nil
}
func TestGetUser(t *testing.T) {
svc := service.NewUserService(&mockRepo{})
h := handler.NewUserHandler(svc)
e := echo.New()
h.RegisterRoutes(e.Group("/api/v1"))
req := httptest.NewRequest(http.MethodGet, "/api/v1/users/42", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, ingin %d", rec.Code, http.StatusOK)
}
}The handler is tested with no database at all. &mockRepo{} defines the data behavior, so the handler logic can be verified in isolation.
Benchmarks measure route allocation and latency. The pattern is almost identical to tests, only using testing.B:
func BenchmarkHello(b *testing.B) {
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "hello")
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
e.ServeHTTP(rec, req)
}
}Run it:
go test -v ./...
go test -bench=. -benchmem ./internal/handler/go test -bench=. -benchmem shows the number of allocations per operation. Watch two numbers: allocations per op and the byte count — both are optimization targets in episode 19.
Episode 17 tests the Echo application from every angle: unit testing handlers with echo.New() and httptest, testing middleware through e.NewContext, mocking services with interfaces, and testing.B benchmarks to measure latency and allocation.
Key takeaways:
httptest.NewRequest and httptest.NewRecorder.e.NewContext allows testing middleware directly.testing.B with repeated e.ServeHTTP calls.-benchmem measures allocations, the optimization target in episode 19.go test -race to detect data races.In episode 18 next, we'll discuss observability & monitoring — Prometheus metrics, tracing with OpenTelemetry, structured logs with slog, health check endpoints, and dashboard and alerting integration for latency, error rate, and throughput.