Reference · เก็บไว้เปิดตอนเขียนโค้ด

Go Day 5 — Cheat Sheet

ครอบคลุมสไลด์ทั้ง 50 หน้า · ตัวอย่างทุกอันรันจริงบน Go 1.26 + testify v1.11.1 แล้ว · พิมพ์ลงกระดาษได้

สารบัญ

  1. กฎการตั้งชื่อ & package
  2. คำสั่ง go test
  3. method ของ testing.T
  4. Table-Driven Test
  5. Testify — assert / require
  6. Testify — mock & suite
  7. TDD & AI workflow
  8. ตารางกับดักรวม

1 · กฎการตั้งชื่อ & package

สิ่งที่ต้องทำถ้าทำผิด
ไฟล์ลงท้าย _test.go[no test files] — เงียบสนิท ไม่มี error
ฟังก์ชันขึ้นต้น Test + ตัวถัดไปเป็นตัวใหญ่/_ไม่ถูกรัน
รับ t *testing.T ตัวเดียว ไม่คืนค่าcompile error
admin_handler.go
admin_handler_test.go     ← วางคู่กันใน folder เดียวกัน

Internal vs External test package

package calc (internal)package calc_test (external)
import package ตัวเอง❌ ไม่ต้อง✅ ต้อง
เทส unexported✅ ได้❌ name X not exported by package calc
มุมมองคนในคนนอก — เหมือนผู้ใช้จริง

Build tag — แยก integration test

//go:build integration
                          ← gofmt ต้องการบรรทัดว่างตรงนี้
package user
เขียนแบบผลรันจริง
อยู่บนสุด + มีบรรทัดว่าง✅ ถูกต้อง
อยู่บนสุด + ไม่มีบรรทัดว่าง⚠️ ยังทำงาน แต่ gofmt -l ฟ้อง
อยู่ใต้ package💥 misplaced compiler directive

2 · คำสั่ง go test

go test [options] [packages]
packagesความหมาย
.folder ปัจจุบัน
./...ทุก folder ลงไปทุกชั้น — ใช้บ่อยสุด
./internal/userระบุ folder
Optionทำอะไร
-vโชว์ชื่อเทสทุกตัว + t.Log
-run <regex>รันเฉพาะที่ชื่อตรง (เจาะ subtest ด้วย / ได้)
-covercoverage: 42.9% of statements
-coverprofile=c.outบันทึกลงไฟล์
-count=1ล้าง cache · -count=10 จับ flaky test
-shortเปิด short mode → testing.Short() เป็น true
-raceจับ data race
-tags <tag>มี s! สไลด์เขียน -tag ซึ่งใช้ไม่ได้
-failfastหยุดทันทีที่มีเทสแรกพัง
go test -v -run 'TestDiscount' .                # ทั้งฟังก์ชัน
go test -v -run 'TestDiscount/ลด_10' .        # เจาะ subtest
go test -v -run 'TestUser|TestOrder' .        # regex OR

go test -coverprofile=c.out .
go tool cover -func=c.out                     # ตาราง
go tool cover -html=c.out                     # หน้าเว็บระบายสี

3 · method ของ testing.T

Methodทำอะไรหยุดเทสไหม
t.Error / t.Errorfทำให้ fail แล้วไปต่อ❌
t.Fatal / t.Fatalfทำให้ fail แล้วหยุด✅
t.Log / t.Logfข้อความดีบัก — เห็นเฉพาะ -v หรือตอน fail❌
t.Skip / t.Skipfข้ามเทสนี้ → --- SKIP:✅
t.Failed()ถามว่าตอนนี้ fail ไปหรือยัง (bool)❌
t.Run(name, fn)สร้าง subtest — ได้ t ตัวใหม่❌
t.Parallel()จอดเทสไว้ แล้วปล่อยวิ่งพร้อมกันทีหลัง—
t.Cleanup(fn)ลงทะเบียนงานเก็บกวาด — LIFO รันแม้ fail❌
t.Helper()บอกว่าฟังก์ชันนี้เป็น helper → error ชี้ที่คนเรียก❌
t.TempDir()folder ชั่วคราวที่ลบให้เองตอนจบ❌
t.Setenv(k, v)ตั้ง env var แล้วคืนค่าเดิมตอนจบ❌

Error vs Fatal — กฎเลือก

user, err := GetUser("U-101")
if err != nil {
    t.Fatalf("GetUser พัง: %v", err)   // Fatal — บรรทัดล่างจะ panic ถ้าไปต่อ
}
if user.Name != "Alice" {
    t.Errorf("Name = %q; want Alice", user.Name)   // Error — อยากเห็นทุกจุดที่ผิด
}

t.Cleanup vs defer

defert.Cleanup
รันเมื่อฟังก์ชันที่เขียน defer จบเทสจบ
เขียนใน helper❌ รันทันทีที่ helper จบ✅ รอจนเทสจบ
ลำดับ · รันตอน failLIFO · ✅LIFO · ✅
func newTestDB(t *testing.T) *DB {
    t.Helper()
    db := openTempDB()
    t.Cleanup(func() { db.Close() })   // defer จะปิดเร็วเกินไป
    return db
}

t.Helper() — ผลรันจริง

ไม่ใส่ → parallel2_test.go:43: got 5, want 10   // ชี้ในตัว helper (ไร้ประโยชน์)
ใส่   → parallel2_test.go:59: got 5, want 10   // ชี้บรรทัดที่เรียก (ถูก)

t.Parallel() — กลไกและกับดักการวัดเวลา

// ❌ วัดผิด — Parallel คืนการควบคุมทันที
t.Run("group", func(t *testing.T) {
    start := time.Now()
    for ... { t.Run(..., func(t *testing.T){ t.Parallel(); ... }) }
    t.Logf("%v", time.Since(start))   // ได้ 139µs — หลอกตา!
})

// ✅ วัดถูก — วัดนอก group
start := time.Now()
t.Run("group", func(t *testing.T) { /* สร้าง parallel subtest */ })
t.Logf("%v", time.Since(start))       // ได้ 201ms — จริง
วิธีผลรันจริง (5 เคส × 200ms)
sequential1.006 s
parallel วัดถูก201 ms (เร็วขึ้น 5 เท่า)
parallel วัดแบบในสไลด์139 µs ← ไม่ใช่เวลาจริง

4 · Table-Driven Test

func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name    string      // ← ขาดไม่ได้ ไม่งั้นไม่รู้ว่าเคสไหนพัง
        price   int
        percent int
        want    int
        wantErr bool
    }{
        {"ลด 10%",          100,  10,  90, false},
        {"ไม่ลด",            100,   0, 100, false},
        {"เปอร์เซ็นต์ติดลบ",  100,  -5,   0,  true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := CalculateDiscount(tt.price, tt.percent)
            if (err != nil) != tt.wantErr {
                t.Fatalf("err = %v; wantErr %v", err, tt.wantErr)
            }
            if got != tt.want {
                t.Errorf("CalculateDiscount(%d, %d) = %d; want %d",
                    tt.price, tt.percent, got, tt.want)
            }
        })
    }
}

5 · Testify — assert / require

go get github.com/stretchr/testify && go mod tidy   // tidy ขาดไม่ได้
assert.Xrequire.X
เทียบเท่ากับt.Errorf — ไปต่อt.Fatalf — หยุด
ใช้กับตรวจผลลัพธ์หลายจุดเงื่อนไขก่อนหน้า
result, err := CalculateDiscount(100, 10)
require.NoError(t, err)          // ถ้าพังก็ไม่ต้องไปต่อ
assert.Equal(t, 90, result)      // ตรวจผล

แคตตาล็อก (รันผ่านจริงบน v1.11.1)

// ค่าปกติ
assert.Equal(t, expected, actual)               // เท่ากัน (type ต้องตรง!)
assert.NotEqual(t, unexpected, actual)
assert.ElementsMatch(t, []int{3,1,2}, list)     // slice เท่ากัน ไม่สนลำดับ

// สถานะ
assert.Nil(t, err) · assert.NotNil(t, obj)
assert.Empty(t, slice) · assert.Len(t, slice, 3)
assert.True(t, ok) · assert.False(t, failed)

// error — อย่าเทียบข้อความ!
require.Error(t, err) · assert.NoError(t, err)
assert.ErrorIs(t, err, ErrNotFound)              // ← แม่นกว่า (ตาม chain)
assert.ErrorAs(t, err, &appErr)
assert.ErrorContains(t, err, "not found")      // หลวม ใช้เมื่อจำเป็น

// float — ห้ามใช้ Equal!
assert.InDelta(t, 3.14, result, 0.001)

// slice/map/string
assert.Contains(t, "hello world", "world")
assert.Contains(t, []int{1,2,3}, 2)
assert.Contains(t, map[string]int{"a":1}, "a")

// เปรียบเทียบ · ข้อความเสริม
assert.Greater(t, a, b) · assert.Less(t, a, b)
assert.Equal(t, 10, x, "input ตอนนั้นคือ %d", input)

assert.Equal เทียบทั้ง type และค่า (ฐานคือ reflect.DeepEqual) — ผลรันจริง:

assert.Equal(t, 90.0, result)   // result เป็น int
→ expected: float64(90)  actual: int(90)   ✗ (สไลด์เขียนแบบนี้!)

var g Grade = "A"
assert.Equal(t, "A", g)
→ expected: string("A")  actual: Grade("A")   ✗

6 · Testify — mock & suite

DI ก่อน แล้วค่อย mock ได้

type Store interface { Save(r Result) error }

type Calculator struct { store Store }
func NewCalculator(s Store) *Calculator { return &Calculator{store: s} }
type MockStore struct { mock.Mock }

func (m *MockStore) Save(r Result) error {
    args := m.Called(r)
    return args.Error(0)
}

mockStore := new(MockStore)
mockStore.On("Save", Result{A:2, B:3, Sum:5}).Return(nil)
calc := NewCalculator(mockStore)
result, err := calc.Add(2, 3)
mockStore.AssertExpectations(t)
// จับคู่ argument แบบหลวม
mock.Anything · mock.AnythingOfType("main.Result")

// ตรวจการเรียก
m.AssertExpectations(t)                 // ทุกอันที่ On ไว้ถูกเรียกครบ
m.AssertCalled(t, "Save", mock.Anything)
m.AssertNotCalled(t, "Update")
m.AssertNumberOfCalls(t, "Save", 2)

// จำกัดจำนวนครั้ง
.Once() · .Twice() · .Times(3)

ลืม .On() แล้วเรียก method → panic แต่ข้อความบอกวิธีแก้พร้อมค่า argument ที่ได้รับจริง:
mock: I don't know what to return because the method call was unexpected.

Suite

type UserServiceSuite struct {
    suite.Suite
    store *MockUserStore
    svc   *UserService
}

func (s *UserServiceSuite) SetupTest() {
    s.store = new(MockUserStore)      // สร้างใหม่ทุกเทส
    s.svc = NewUserService(s.store)
}

func (s *UserServiceSuite) TestFindUser() {
    s.store.On("GetByID", 1).Return(&User{Name: "Bob"}, nil)
    user, err := s.svc.FindUser(1)
    s.Require().NoError(err)             // ⚠️ ไม่ต้องส่ง t
    s.Assert().Equal("Bob", user.Name)
}

func TestUserServiceSuite(t *testing.T) {   // ← ขาดไม่ได้
    suite.Run(t, new(UserServiceSuite))
}
Hookรันเมื่อ
SetupSuite()ครั้งเดียวก่อนทุกเทส
SetupTest()ก่อนทุกเทส
BeforeTest(suiteName, testName)ก่อนแต่ละเทส (รู้ชื่อ)
AfterTest(suiteName, testName)หลังแต่ละเทส
TearDownTest()หลังทุกเทส
TearDownSuite()ครั้งเดียวหลังทุกเทสจบ

7 · TDD & AI workflow

   [Red] ──▶ [Green] ──▶ [Refactor] ──┐
     ▲                                │
     └────────────────────────────────┘
เขียนเทสให้พัง   แก้ให้ผ่าน    ทำให้สวย (เทสยังเขียว)
ขั้นกฎเหล็ก
🔴 Redห้ามเขียน production code ก่อนเห็นไฟแดง
🟢 Greenเขียนน้อยที่สุดที่ทำให้ผ่าน — สวยทีหลังได้
🔵 Refactorเทสต้องเขียวตลอด · ห้ามแก้เทสไปพร้อมกัน

Red มีสองแบบ — มีแบบเดียวที่ใช่

// 🚫 ยังไม่ใช่ Red — เทสยังไม่เคยรันเลย
./division_test.go:6:14: undefined: Division
FAIL	tdd [build failed]

// ✅ Red ที่ต้องการ — เทสรันแล้วพังด้วยเหตุผลที่ถูก
--- FAIL: TestDivision (0.00s)
    division_test.go:11: Division(10,2) = 0; want 5

AI workflow

1. คุณ: เขียน table-driven test ระบุพฤติกรรมทุกเคส (รวมเคสขอบ)
2. คุณ: รันให้เห็นแดง — พิสูจน์ว่าเทสจับได้จริง
3. AI:  เขียน implementation ให้ผ่านเทส (ห้ามแก้ไฟล์เทส)
4. AI:  รัน go test เอง แก้เอง จนเขียว
5. คุณ: อ่านโค้ด — มัน "โกง" เทสหรือเปล่า?
6. คุณ: เพิ่มเคส → กลับข้อ 2

8 · ตารางกับดักรวม — อ่านก่อนส่ง PR

สิ่งที่เห็น / เขียนสิ่งที่คุณคิดสิ่งที่เกิดจริง
ไฟล์ mytests.go มี func TestX รันเทสได้ [no test files] เงียบสนิท
package calc_test เรียก double() อยู่ folder เดียวกันน่าจะได้ 💥 name double not exported
//go:build อยู่ใต้ package อยู่ไฟล์เดียวกันก็พอ 💥 misplaced compiler directive
go test -tag=integration ตามที่สไลด์เขียน 💥 flag provided but not defined: -tag · ใช้ -tags
ok (cached) เทสผ่านแล้ว ไม่ได้รันจริง · ใช้ -count=1
coverage: 100% ไม่มีบั๊กแล้ว แค่ทุกบรรทัดถูกรันผ่าน — assert อาจไม่มีเลย
t.Error แล้วใช้ค่าต่อ หยุดแล้ว 💥 บรรทัดถัดไป panic (nil) · ใช้ t.Fatal
t.Fatal ใน goroutine หยุดเทส หยุดแค่ goroutine · ผลลัพธ์เพี้ยน
fmt.Println ในเทส ดีบักสะดวก พิมพ์ตลอด ไม่มีเลขบรรทัด · ใช้ t.Log
helper ไม่ใส่ t.Helper() ไม่เป็นไร error ทุกอันชี้บรรทัดเดียวกันในตัว helper
defer db.Close() ใน test helper ปิดตอนเทสจบ ปิดทันทีที่ออกจาก helper · ใช้ t.Cleanup
วัดเวลาในบล็อกที่มี t.Parallel() ได้เวลารันจริง ได้ ~139µs (แค่เวลาจอดคิว) · วัดนอกบล็อก
t.Parallel() + ตัวแปร global เร็วขึ้น flaky test · ตรวจด้วย -race -count=10
ตารางเทสไม่มี field name ใช้ index ก็ได้ case 1 failed — ไม่รู้ว่าเคสอะไร
assert.Equal(t, 90.0, intResult) ค่าเท่ากันก็ผ่าน 💥 float64(90) ≠ int(90) (สไลด์เขียนแบบนี้)
assert.Equal(t, 0.3, a+b) (float) 0.1+0.2 = 0.3 💥 ได้ 0.30000000000000004 · ใช้ InDelta
assert.Equal(t, "A", grade) (custom type) ค่าเป็น "A" เหมือนกัน 💥 string("A") ≠ Grade("A")
ลืม go mod tidy หลัง go get testify ลงเสร็จแล้ว 💥 missing go.sum entry for … go-spew
ใน suite เรียก assert.Equal(t, …) เหมือนเทสปกติ ไม่มี t ให้ใช้ · ใช้ s.Assert()
เขียนเทส suite ให้ทำงานตามลำดับ รันตามลำดับในไฟล์ เรียงตามตัวอักษร → เทสพังแบบเดาไม่ได้
ลืม mockStore.On(...) mock คืน zero value 💥 panic — แต่ข้อความบอกวิธีแก้ให้
build failed ตอนทำ TDD นี่คือ Red แล้ว เทสยังไม่เคยรัน · เขียนโครงเปล่าให้คอมไพล์ผ่านก่อน