การทดสอบ Echo Handler
เปรียบเทียบ philosophy การทดสอบ
หัวข้อที่มีชื่อว่า “เปรียบเทียบ philosophy การทดสอบ”ใน Node คุณมักจะ spin up แอป NestJS/Express กับ Supertest แล้วยิง HTTP request เข้าไป ส่วนใน Go package net/http/httptest ให้คุณสร้าง in-memory HTTP recorder (httptest.ResponseRecorder) ได้โดยไม่ต้อง bind network port เทสจึงรันเร็ว, deterministic และไม่ต้อง cleanup
| สิ่งที่ต้องการ | Node.js / Jest + Supertest | Go / testing + httptest |
|---|---|---|
| Test runner | Jest | go test (stdlib) |
| HTTP simulation | supertest(app).get('/books') | httptest.NewRecorder() + เรียก handler |
| Assertion | expect(res.statusCode).toBe(200) | if res.Code != 200 { t.Errorf(...) } |
| Mock dependency | Jest jest.fn() | Interface + test double struct |
Handler test พื้นฐาน
หัวข้อที่มีชื่อว่า “Handler test พื้นฐาน”package handler_test
import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing"
"github.com/labstack/echo/v4" "github.com/you/books-api/internal/handler")
func TestGetBook_OK(t *testing.T) { e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/books/1", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("1")
h := handler.NewBookHandler(&fakeBookStore{}) if err := h.Get(c); err != nil { t.Fatalf("handler returned error: %v", err) }
if rec.Code != http.StatusOK { t.Errorf("expected 200, got %d", rec.Code) }
var body map[string]any if err := json.NewDecoder(rec.Body).Decode(&body); err != nil { t.Fatalf("could not decode body: %v", err) } if body["title"] != "Go in Action" { t.Errorf("unexpected title: %v", body["title"]) }}Mock store ด้วย interface
หัวข้อที่มีชื่อว่า “Mock store ด้วย interface”ประกาศ interface BookStore ไว้ใน handler package แล้วในเทสค่อย implement interface นั้นด้วย struct ที่คืนข้อมูลแบบควบคุมได้ — ไม่ต้องพึ่งฐานข้อมูลจริง
// Jest — mock serviceconst mockBooksService = { findById: jest.fn().mockResolvedValue({ id: '1', title: 'Go in Action', }),};
const module = await Test.createTestingModule({ controllers: [BooksController], providers: [{ provide: BooksService, useValue: mockBooksService }],}).compile();// Go — interface + test double (ไม่ต้องการ mocking library)
// handler/books.go — ประกาศ interface ที่ handler พึ่งพาtype BookStore interface { FindByID(ctx context.Context, id string) (model.Book, error) Insert(ctx context.Context, req model.CreateBookRequest) (model.Book, error)}
// handler/books_test.go — implementation สำหรับ testtype fakeBookStore struct{}
func (f *fakeBookStore) FindByID(_ context.Context, id string) (model.Book, error) { return model.Book{ID: id, Title: "Go in Action", Author: "Kennedy"}, nil}
func (f *fakeBookStore) Insert(_ context.Context, req model.CreateBookRequest) (model.Book, error) { return model.Book{ID: "new-id", Title: req.Title, Author: req.Author}, nil}ทดสอบ POST handler กับ JSON body
หัวข้อที่มีชื่อว่า “ทดสอบ POST handler กับ JSON body”func TestCreateBook_Created(t *testing.T) { e := echo.New()
body := `{"title":"Go in Action","author":"Kennedy","year":2015}` req := httptest.NewRequest(http.MethodPost, "/books", strings.NewReader(body)) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder() c := e.NewContext(req, rec)
h := handler.NewBookHandler(&fakeBookStore{}) if err := h.Create(c); err != nil { t.Fatalf("handler error: %v", err) }
if rec.Code != http.StatusCreated { t.Errorf("expected 201, got %d: %s", rec.Code, rec.Body.String()) }}ทดสอบ error path
หัวข้อที่มีชื่อว่า “ทดสอบ error path”func TestGetBook_NotFound(t *testing.T) { e := echo.New() req := httptest.NewRequest(http.MethodGet, "/books/999", nil) rec := httptest.NewRecorder() c := e.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("999")
h := handler.NewBookHandler(¬FoundStore{}) err := h.Get(c)
// Echo handler คืน HTTPError แทนการเขียนลง rec โดยตรง var he *echo.HTTPError if !errors.As(err, &he) { t.Fatalf("expected HTTPError, got %T", err) } if he.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", he.Code) }}
type notFoundStore struct{}
func (n *notFoundStore) FindByID(_ context.Context, _ string) (model.Book, error) { return model.Book{}, store.ErrNotFound}
func (n *notFoundStore) Insert(_ context.Context, _ model.CreateBookRequest) (model.Book, error) { return model.Book{}, nil}รัน test
หัวข้อที่มีชื่อว่า “รัน test”# รัน test ทั้งหมดgo test ./...
# พร้อม coverage reportgo test -cover ./...
# Race detector (สำคัญสำหรับ concurrent handler)go test -race ./...
# Verbose outputgo test -v ./internal/handler/...รันบนเครื่องของคุณ — test ต้องการ Echo package ใช้
go test ./...จาก project root ของคุณ