SiamCafe · Blog
Go Testing คืออะไร? สอน Unit Test, Table-Driven Test, Benchmark สำหรับ Go Developer 2026
บทความทั่วไป

Go Testing คืออะไร? สอน Unit Test, Table-Driven Test, Benchmark สำหรับ Go Developer 2026

เผยแพร่ May 28, 2026

Go ออกแบบ Testing มาเป็นส่วนหนึ่งของภาษาตั้งแต่แรก — testing package อยู่ใน Standard library, go test เป็น Built-in command ไม่ต้องติดตั้ง Framework เพิ่ม ปรัชญาของ Go คือ "Testing should be simple" ไม่ต้องมี Magic, Annotation, หรือ Decorator แค่เขียน Function ที่ชื่อขึ้นต้นด้วย Test ก็พอ

Go Testing พื้นฐาน

Go Testing คืออะไร? สอน Unit Test, Table-Driven Test, Benchmark สำหรับ Go Developer 2026

กฎการตั้งชื่อ

  • ไฟล์ Test ต้องลงท้ายด้วย _test.go
  • Function Test ต้องขึ้นต้นด้วย Test (Capital T)
  • Parameter ต้องเป็น *testing.T
  • ไฟล์ Test อยู่ใน Package เดียวกับ Code ที่ทดสอบ (หรือ _test package สำหรับ Black-box testing)

เขียน Test แรก

// math.go
package math

func Add(a, b int) int {
    return a + b
}

func Subtract(a, b int) int {
    return a - b
}

func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }
    return a / b, nil
}
// math_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

func TestSubtract(t *testing.T) {
    result := Subtract(10, 3)
    if result != 7 {
        t.Errorf("Subtract(10, 3) = %d; want 7", result)
    }
}

func TestDivide(t *testing.T) {
    result, err := Divide(10, 2)
    if err != nil {
        t.Fatalf("Divide(10, 2) returned error: %v", err)
    }
    if result != 5.0 {
        t.Errorf("Divide(10, 2) = %f; want 5.0", result)
    }
}

func TestDivideByZero(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Error("Divide(10, 0) should return error")
    }
}
# รัน Test
go test ./...                    # ทุก Package
go test ./math                   # เฉพาะ Package math
go test -v ./...                 # Verbose output
go test -run TestAdd ./math      # เฉพาะ Test ที่ match "TestAdd"
go test -count=1 ./...           # ไม่ใช้ Cache

Table-Driven Tests — วิธี Idiomatic Go

Table-Driven Test คือ Pattern ที่ Go developers ใช้มากที่สุด แทนที่จะเขียน Test function แยกสำหรับแต่ละ Case ให้รวมทุก Case ไว้ใน Table (Slice of struct) แล้ว Loop ทดสอบ:

อ่านเพิ่ม: pnpm คืออะไร? Package Manager ที่เร็วและประหยัด Disk กว่า np · อ่านเพิ่ม: Rust Programming คืออะไร? สอน Rust จาก 0 สำหรับ Developer ที · อ่านเพิ่ม: Docker Compose 2026 สร้าง Home Lab Self-Hosted Services ที่บ

func TestAdd_TableDriven(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -2, -3},
        {"mixed", -1, 5, 4},
        {"zeros", 0, 0, 0},
        {"large numbers", 1000000, 2000000, 3000000},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

func TestDivide_TableDriven(t *testing.T) {
    tests := []struct {
        name      string
        a, b      float64
        expected  float64
        expectErr bool
    }{
        {"normal division", 10, 2, 5.0, false},
        {"decimal result", 7, 2, 3.5, false},
        {"divide by zero", 10, 0, 0, true},
        {"negative", -10, 2, -5.0, false},
        {"both negative", -10, -2, 5.0, false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Divide(tt.a, tt.b)
            if tt.expectErr {
                if err == nil {
                    t.Error("expected error but got nil")
                }
                return
            }
            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if result != tt.expected {
                t.Errorf("Divide(%f, %f) = %f; want %f",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}
ทำไมต้อง Table-Driven?
  • เพิ่ม Test case ง่าย — แค่เพิ่ม Row ใน Table
  • ลด Code ซ้ำซ้อน — Logic ทดสอบเขียนครั้งเดียว
  • t.Run() ทำให้เห็นว่า Case ไหน Fail
  • เป็นมาตรฐานของ Go community ทุกคนอ่านเข้าใจ

Subtests (t.Run) และ Test Helpers (t.Helper)

// Subtests — จัดกลุ่ม Test
func TestUserService(t *testing.T) {
    t.Run("Create", func(t *testing.T) {
        t.Run("valid user", func(t *testing.T) { /* ... */ })
        t.Run("duplicate email", func(t *testing.T) { /* ... */ })
        t.Run("invalid email", func(t *testing.T) { /* ... */ })
    })

    t.Run("Get", func(t *testing.T) {
        t.Run("existing user", func(t *testing.T) { /* ... */ })
        t.Run("non-existing user", func(t *testing.T) { /* ... */ })
    })
}

// Run เฉพาะ subtest:
// go test -run TestUserService/Create/valid_user

// Test Helper — ฟังก์ชันช่วย (ไม่นับเป็น Test)
func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper() // ← สำคัญ! ทำให้ Error message แสดง Line ของ Caller ไม่ใช่ Helper
    if got != want {
        t.Errorf("got %v; want %v", got, want)
    }
}

func TestWithHelper(t *testing.T) {
    assertEqual(t, Add(2, 3), 5)     // Error จะชี้ไปที่บรรทัดนี้
    assertEqual(t, Add(-1, 1), 0)
}

Testify — Popular Testing Library

// go get github.com/stretchr/testify

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestWithTestify(t *testing.T) {
    // assert — ถ้า Fail ยังรัน Test ต่อ
    assert.Equal(t, 5, Add(2, 3))
    assert.NotEqual(t, 0, Add(2, 3))
    assert.True(t, Add(2, 3) > 0)
    assert.Contains(t, "hello world", "world")
    assert.Nil(t, err)
    assert.NotNil(t, result)
    assert.Len(t, slice, 3)

    // require — ถ้า Fail หยุด Test ทันที (เหมือน t.Fatal)
    result, err := Divide(10, 2)
    require.NoError(t, err)       // ถ้า Error → หยุดเลย ไม่ต้อง check result
    require.Equal(t, 5.0, result)

    // assert.Error สำหรับ Expected errors
    _, err = Divide(10, 0)
    assert.Error(t, err)
    assert.ErrorContains(t, err, "divide by zero")
}

httptest — ทดสอบ HTTP Handlers

// handler.go
func HealthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

func UserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "id required", http.StatusBadRequest)
        return
    }
    json.NewEncoder(w).Encode(map[string]string{"id": id, "name": "Test User"})
}

// handler_test.go
import (
    "net/http"
    "net/http/httptest"
    "testing"
    "encoding/json"
)

func TestHealthHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/health", nil)
    rec := httptest.NewRecorder()

    HealthHandler(rec, req)

    // Check status code
    if rec.Code != http.StatusOK {
        t.Errorf("status = %d; want 200", rec.Code)
    }

    // Check response body
    var body map[string]string
    json.NewDecoder(rec.Body).Decode(&body)
    if body["status"] != "ok" {
        t.Errorf("status = %s; want ok", body["status"])
    }
}

func TestUserHandler_NoID(t *testing.T) {
    req := httptest.NewRequest("GET", "/user", nil)
    rec := httptest.NewRecorder()

    UserHandler(rec, req)

    if rec.Code != http.StatusBadRequest {
        t.Errorf("status = %d; want 400", rec.Code)
    }
}

// ทดสอบกับ Real HTTP server
func TestUserHandler_Integration(t *testing.T) {
    server := httptest.NewServer(http.HandlerFunc(UserHandler))
    defer server.Close()

    resp, err := http.Get(server.URL + "/user?id=123")
    if err != nil {
        t.Fatalf("request failed: %v", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        t.Errorf("status = %d; want 200", resp.StatusCode)
    }
}

Testing with Database (testcontainers-go)

// go get github.com/testcontainers/testcontainers-go

import (
    "context"
    "testing"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
)

func setupPostgres(t *testing.T) (string, func()) {
    t.Helper()
    ctx := context.Background()

    container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: testcontainers.ContainerRequest{
            Image:        "postgres:16-alpine",
            ExposedPorts: []string{"5432/tcp"},
            Env: map[string]string{
                "POSTGRES_USER":     "test",
                "POSTGRES_PASSWORD": "test",
                "POSTGRES_DB":       "testdb",
            },
            WaitingFor: wait.ForLog("database system is ready to accept connections").
                WithOccurrence(2),
        },
        Started: true,
    })
    if err != nil {
        t.Fatalf("failed to start container: %v", err)
    }

    host, _ := container.Host(ctx)
    port, _ := container.MappedPort(ctx, "5432")
    dsn := fmt.Sprintf("postgres://test:test@%s:%s/testdb?sslmode=disable", host, port.Port())

    cleanup := func() { container.Terminate(ctx) }
    return dsn, cleanup
}

func TestUserRepository(t *testing.T) {
    dsn, cleanup := setupPostgres(t)
    defer cleanup()

    db, err := sql.Open("postgres", dsn)
    require.NoError(t, err)

    repo := NewUserRepository(db)
    // ... test CRUD operations ...
}

Coverage

รัน Test พร้อม Coverage

go test -cover ./...

Output: coverage: 85.2% of statements

สร้าง Coverage profile

go test -coverprofile=coverage.out ./...

ดู Coverage แบบ HTML (เปิด Browser)

go tool cover -html=coverage.out

ดู Coverage ทีละ Function

go tool cover -func=coverage.out

Coverage เฉพาะ Package

go test -coverprofile=coverage.out -coverpkg=./... ./...

ตั้ง Minimum coverage ใน CI:

go test -coverprofile=coverage.out ./...

COVERAGE=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%')

if (( $(echo "$COVERAGE < 80" | bc -l) )); then

echo "Coverage $COVERAGE% is below 80%"

exit 1

fi

Benchmarking

// Benchmark functions ขึ้นต้นด้วย Benchmark + parameter *testing.B

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

func BenchmarkDivide(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Divide(10.0, 3.0)
    }
}

// Benchmark with allocation reporting
func BenchmarkStringConcat(b *testing.B) {
    b.ReportAllocs()  // รายงาน Memory allocation
    for i := 0; i < b.N; i++ {
        s := ""
        for j := 0; j < 100; j++ {
            s += "a"
        }
    }
}

func BenchmarkStringBuilder(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        var sb strings.Builder
        for j := 0; j < 100; j++ {
            sb.WriteString("a")
        }
        _ = sb.String()
    }
}

รัน Benchmark

go test -bench=. ./...

go test -bench=BenchmarkAdd ./...

go test -bench=. -benchmem ./... # รวม Memory allocation

go test -bench=. -count=5 ./... # รัน 5 ครั้ง (สำหรับ benchstat)

go test -bench=. -benchtime=5s ./... # รันนานขึ้น (default 1s)

Output ตัวอย่าง:

Go Testing คืออะไร? สอน Unit Test, Table-Driven Test, Benchmark สำหรับ Go Developer 2026

BenchmarkAdd-8 1000000000 0.2891 ns/op

BenchmarkStringConcat-8 15241 78542 ns/op 26456 B/op 99 allocs/op

BenchmarkStringBuilder-8 234567 5123 ns/op 752 B/op 7 allocs/op

↑ Builder เร็วกว่า 15 เท่า!

เปรียบเทียบ Benchmark ด้วย benchstat:

go install golang.org/x/perf/cmd/benchstat@latest

go test -bench=. -count=10 ./... > old.txt

(ปรับปรุง Code)

go test -bench=. -count=10 ./... > new.txt

benchstat old.txt new.txt

Fuzz Testing (Go 1.18+)

// Fuzz testing ให้ Go สร้าง Input แบบ Random เพื่อหา Edge case

func FuzzDivide(f *testing.F) {
    // Seed corpus — ตัวอย่าง Input เริ่มต้น
    f.Add(10.0, 2.0)
    f.Add(0.0, 1.0)
    f.Add(-5.0, 3.0)
    f.Add(100.0, 0.1)

    f.Fuzz(func(t *testing.T, a, b float64) {
        if b == 0 {
            // Skip divide by zero — เรารู้ว่า Error
            t.Skip("skip divide by zero")
        }
        result, err := Divide(a, b)
        if err != nil {
            t.Errorf("Divide(%f, %f) returned unexpected error: %v", a, b, err)
        }
        // Check: a / b * b ≈ a (ยกเว้น Floating point precision)
        if math.Abs(result*b-a) > 1e-9 {
            t.Errorf("Divide(%f, %f) = %f; result*b = %f != a", a, b, result, result*b)
        }
    })
}

// Fuzz สำหรับ String parsing
func FuzzParseJSON(f *testing.F) {
    f.Add(`{"name": "test"}`)
    f.Add(`{"age": 25}`)
    f.Add(`[]`)
    f.Add(`""`)

    f.Fuzz(func(t *testing.T, input string) {
        var result interface{}
        err := json.Unmarshal([]byte(input), &result)
        if err != nil {
            return // Invalid JSON — OK
        }
        // ถ้า Parse ได้ ต้อง Marshal กลับได้
        _, err = json.Marshal(result)
        if err != nil {
            t.Errorf("Marshal failed for valid JSON: %v", err)
        }
    })
}
# รัน Fuzz test
go test -fuzz=FuzzDivide ./...
go test -fuzz=FuzzDivide -fuzztime=30s ./...   # รัน 30 วินาที
go test -fuzz=FuzzDivide -fuzztime=1000x ./... # รัน 1000 iterations

# Fuzz corpus ถูกเก็บใน testdata/fuzz/
# ถ้าพบ Bug → Go สร้างไฟล์ใน testdata/fuzz/FuzzDivide/ → ใช้เป็น Regression test

Race Detection

Go มี Built-in Race detector — ตรวจจับ Data race

รัน Test with Race detection

go test -race ./...

Build with Race detection

go build -race -o myapp

Run with Race detection

go run -race main.go

Output เมื่อพบ Race:

WARNING: DATA RACE

Write at 0x00c0000b4018 by goroutine 7:

main.increment()

/path/main.go:15 +0x48

Previous read at 0x00c0000b4018 by goroutine 8:

main.getCount()

/path/main.go:20 +0x38

ใช้ -race ตอน Test เสมอ! อย่าลืมใส่ใน CI

(อย่าใช้ใน Production — ช้าลง 2-10x, ใช้ Memory มากขึ้น 5-10x)

Test Fixtures และ Golden Files

// Test fixtures — ข้อมูลทดสอบเก็บใน testdata/
// Go จะ ignore directory ชื่อ testdata ตอน Build

// testdata/input.json
// testdata/expected_output.json

func TestProcessData(t *testing.T) {
    // Read input fixture
    input, err := os.ReadFile("testdata/input.json")
    require.NoError(t, err)

    result := ProcessData(input)

    // Golden file pattern — ถ้า -update flag → เขียน expected output ใหม่
    golden := "testdata/expected_output.json"
    if *update {
        os.WriteFile(golden, result, 0644)
    }

    expected, err := os.ReadFile(golden)
    require.NoError(t, err)
    assert.JSONEq(t, string(expected), string(result))
}

// Flag สำหรับ Update golden files
var update = flag.Bool("update", false, "update golden files")

func TestMain(m *testing.M) {
    flag.Parse()
    os.Exit(m.Run())
}

// รัน: go test -update ./...  (อัปเดต Golden files)
// รัน: go test ./...          (เปรียบเทียบกับ Golden files)

TestMain — Setup/Teardown

// TestMain ใช้สำหรับ Setup/Teardown ที่ทำครั้งเดียว (ไม่ใช่ทุก Test)

func TestMain(m *testing.M) {
    // Setup — ทำก่อน Test ทั้งหมด
    fmt.Println("=== SETUP ===")
    db := setupDatabase()
    defer db.Close()

    // Run all tests
    code := m.Run()

    // Teardown — ทำหลัง Test ทั้งหมด
    fmt.Println("=== TEARDOWN ===")
    cleanupDatabase()

    os.Exit(code)
}

// สำหรับ Per-test setup/teardown ใช้ t.Cleanup():
func TestSomething(t *testing.T) {
    resource := createResource()
    t.Cleanup(func() {
        resource.Close() // ถูกเรียกหลัง Test จบ (แม้ Fail)
    })
    // ... test ...
}

go test Flags

Flagหน้าที่ตัวอย่าง
-vVerbose outputgo test -v ./...
-run REGEXรันเฉพาะ Test ที่ matchgo test -run TestAdd ./...
-count Nรัน N ครั้ง (ไม่ Cache)go test -count=1 ./...
-parallel Nจำนวน Test ที่รัน Parallelgo test -parallel 4 ./...
-raceเปิด Race detectorgo test -race ./...
-coverแสดง Coveragego test -cover ./...
-coverprofileสร้าง Coverage filego test -coverprofile=c.out ./...
-bench REGEXรัน Benchmarkgo test -bench=. ./...
-benchmemรายงาน Memory ใน Benchmarkgo test -bench=. -benchmem ./...
-fuzz REGEXรัน Fuzz testgo test -fuzz=Fuzz ./...
-timeout DTimeout (default 10m)go test -timeout 30s ./...
-shortSkip Long-running testsgo test -short ./...
-failfastหยุดทันทีเมื่อ Test แรก Failgo test -failfast ./...

Integration Tests

// แยก Integration test ด้วย Build tag หรือ -short flag

// === วิธีที่ 1: ใช้ -short flag ===
func TestDatabaseIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test in short mode")
    }
    // ... integration test ที่ต้อง Database จริง ...
}

// รัน: go test ./...         (รัน Integration test ด้วย)
// รัน: go test -short ./...  (ข้าม Integration test)

// === วิธีที่ 2: ใช้ Build tag ===
//go:build integration

package mypackage

func TestDatabaseIntegration(t *testing.T) {
    // ... integration test ...
}

// รัน: go test -tags=integration ./...

CI Integration

# GitHub Actions — Go test pipeline
# .github/workflows/test.yml

name: Go Test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Run tests
        run: go test -v -race -coverprofile=coverage.out ./...

      - name: Check coverage
        run: |
          COVERAGE=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%')
          echo "Coverage: $COVERAGE%"
          if (( $(echo "$COVERAGE < 80" | bc -l) )); then
            echo "Coverage below 80%!"
            exit 1
          fi

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          file: ./coverage.out

Testing Best Practices

  1. ใช้ Table-Driven Tests เป็นหลัก: มาตรฐานของ Go community ง่ายต่อการเพิ่ม Case
  2. Test ชื่อ function ให้สื่อ: TestUser_Create_DuplicateEmail_ReturnsError ดีกว่า TestUser2
  3. ใช้ t.Helper(): สำหรับ Helper function เพื่อให้ Error message แสดง Line ที่ถูกต้อง
  4. ใช้ t.Parallel(): สำหรับ Test ที่ Independent เร่งเวลารัน
  5. ใช้ -race ทุกครั้ง: ตอน Dev และ CI เสมอ จับ Data race ก่อน Production
  6. Coverage ไม่ใช่ทุกอย่าง: 80% coverage ที่ Test สิ่งสำคัญ ดีกว่า 100% coverage ที่ Test แค่ Happy path
  7. Benchmark เมื่อจำเป็น: อย่า Optimize ก่อน Benchmark ถ้าไม่มีตัวเลข ≠ ไม่มี Optimization
  8. Fuzz test สำหรับ Parser/Validator: ฟังก์ชันที่รับ Input จาก User ควร Fuzz

Go Testing ง่ายกว่าภาษาอื่นมาก เพราะทุกอย่าง Built-in ไม่ต้อง Config ไม่ต้องติดตั้ง Framework แค่ go test ./... ก็เริ่มได้ เริ่มเขียน Test วันนี้ แล้วคุณจะรู้สึกมั่นใจทุกครั้งที่ Deploy Code ขึ้น Production