Go Testing ในปี 2026: Unit Test, Mock และคำถามสัมภาษณ์ทางเทคนิค

เชี่ยวชาญ Go testing ด้วย table-driven test, mock และ pattern ที่ผู้สัมภาษณ์คาดหวัง ตัวอย่างการใช้งานจริงกับ testify และ gomock

Go Testing ในปี 2026: Unit Test, Mock และคำถามสัมภาษณ์ทางเทคนิค

การทดสอบใน Go พึ่งพา package มาตรฐาน testing ซึ่งมาพร้อมกับทุกการติดตั้ง Go และไม่ต้องการ dependency ภายนอก ต่างจาก framework ในภาษาอื่น แนวทางของ Go ให้ความสำคัญกับความเรียบง่าย: ไฟล์ test อยู่ในโฟลเดอร์เดียวกับ production code, ฟังก์ชัน test ใช้หลักการตั้งชื่อตามแบบแผน และคำสั่ง go test จัดการการค้นหาและดำเนินการโดยอัตโนมัติ

สิ่งที่ผู้สัมภาษณ์มองหา

ผู้สมัครที่เขียน table-driven test, ใช้ interface สำหรับ dependency injection และเข้าใจว่าเมื่อใด mocking ให้ประโยชน์ เมื่อเทียบกับการเพิ่มความซับซ้อนโดยไม่จำเป็น

การเขียน Unit Test ด้วย Package testing

ทุกไฟล์ test ลงท้ายด้วย _test.go และอยู่ใน package เดียวกับโค้ดที่ต้องการทดสอบ ฟังก์ชัน test เริ่มต้นด้วย Test ตามด้วยชื่อที่ขึ้นต้นด้วยตัวพิมพ์ใหญ่ พารามิเตอร์ *testing.T มี method สำหรับรายงานความล้มเหลว

calculator_test.gogo
package calculator

import "testing"

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

การรัน go test ./... จะดำเนินการ test ทั้งหมดใน module ปัจจุบัน flag -v แสดงชื่อ test แต่ละตัวและสถานะ pass/fail

Table-Driven Test: มาตรฐานของ Go

Table-driven test ลดความซ้ำซ้อนและทำให้การเพิ่ม case ใหม่เป็นเรื่องง่าย แต่ละแถวในตารางแทนหนึ่งสถานการณ์ที่มี input และ output ที่คาดหวัง pattern นี้ปรากฏใน standard library ของ Go และใน production codebase ทั่วอุตสาหกรรม

validator_test.gogo
package validator

import "testing"

func TestValidateEmail(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        wantErr bool
    }{
        {"valid email", "user@example.com", false},
        {"missing at sign", "userexample.com", true},
        {"empty string", "", true},
        {"unicode local part", "user@例え.jp", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.email)
            if (err != nil) != tt.wantErr {
                t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
                    tt.email, err, tt.wantErr)
            }
        })
    }
}

method t.Run สร้าง subtest ที่ทำงานอิสระจากกัน subtest ที่ล้มเหลวจะรายงานว่า case ใดเกิดข้อผิดพลาดโดยไม่หยุด case อื่น

Dependency Injection ผ่าน Interface

Interface ของ Go ช่วยให้สามารถทดสอบโค้ดที่พึ่งพาระบบภายนอก: database, HTTP client, file system การกำหนด interface ที่เล็กที่สุดที่จุดใช้งานช่วยให้สามารถสลับ implementation จริงกับ test double ได้

service.gogo
package order

// Repository defines the data access methods this service needs
type Repository interface {
    FindByID(id string) (*Order, error)
    Save(order *Order) error
}

// Service handles order business logic
type Service struct {
    repo Repository
}

// NewService creates a service with the given repository
func NewService(repo Repository) *Service {
    return &Service{repo: repo}
}

// Process validates and saves an order
func (s *Service) Process(order *Order) error {
    if order.Total <= 0 {
        return ErrInvalidTotal
    }
    return s.repo.Save(order)
}

Service รับ type ใดก็ได้ที่ตรงกับ Repository production code ส่ง implementation ที่ใช้ database ส่วน test ส่ง mock หรือ stub

Mocking ด้วย gomock และ mockgen

package gomock สร้าง mock implementation จากนิยาม interface คำสั่ง mockgen อ่านไฟล์ต้นทางและสร้าง mock struct ที่บันทึกการเรียกและคืนค่าที่กำหนดไว้

bash
# Generate mocks for the Repository interface
mockgen -source=service.go -destination=mocks/repository_mock.go -package=mocks

การใช้ mock ที่สร้างขึ้นใน test:

service_test.gogo
package order

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"

    "yourmodule/order/mocks"
)

func TestService_Process_SavesValidOrder(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    order := &Order{ID: "123", Total: 99.99}

    // Expect Save to be called once with this order, return nil
    mockRepo.EXPECT().
        Save(order).
        Return(nil).
        Times(1)

    svc := NewService(mockRepo)
    err := svc.Process(order)

    assert.NoError(t, err)
}

func TestService_Process_RejectsZeroTotal(t *testing.T) {
    ctrl := gomock.NewController(t)
    mockRepo := mocks.NewMockRepository(ctrl)

    // No calls expected to repo since validation fails first

    svc := NewService(mockRepo)
    err := svc.Process(&Order{ID: "456", Total: 0})

    assert.ErrorIs(t, err, ErrInvalidTotal)
}

mock ตรวจสอบว่า Save ถูกเรียกหนึ่งครั้งด้วย argument ที่คาดหวัง หากโค้ดที่ทดสอบไม่เรียก Save หรือเรียกด้วย argument ที่แตกต่าง test จะล้มเหลว

การใช้ testify สำหรับ Assertion และ Suite

testify มีฟังก์ชัน assertion ที่สร้างข้อความความล้มเหลวที่ชัดเจนกว่าการตรวจสอบ if แบบ manual package assert ดำเนินการต่อหลังจากความล้มเหลว package require หยุด test ทันที

user_test.gogo
package user

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestParseUser(t *testing.T) {
    input := `{"id": "u1", "name": "Alice"}`

    user, err := ParseUser(input)

    require.NoError(t, err, "parsing should not fail")
    assert.Equal(t, "u1", user.ID)
    assert.Equal(t, "Alice", user.Name)
    assert.Empty(t, user.Email, "email should default to empty")
}

การเรียก require.NoError หยุด test หาก parsing ล้มเหลว ป้องกัน nil pointer panic ใน assertion ถัดไป

พร้อมที่จะพิชิตการสัมภาษณ์ Go แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การทดสอบ HTTP Handler ด้วย httptest

package net/http/httptest มี ResponseRecorder สำหรับบันทึก output ของ handler และ Server สำหรับรัน local test server unit test ส่วนใหญ่ใช้ ResponseRecorder เพื่อหลีกเลี่ยง network overhead

handler_test.gogo
package api

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

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

    HealthHandler(rec, req)

    assert.Equal(t, http.StatusOK, rec.Code)
    assert.JSONEq(t, `{"status": "ok"}`, rec.Body.String())
}

สำหรับ handler ที่เรียกบริการภายนอก ให้ inject mock client หรือใช้ httptest.NewServer เพื่อสร้าง backend จำลอง

คำถามสัมภาษณ์ที่พบบ่อยเกี่ยวกับ Go Testing

ผู้สัมภาษณ์ประเมินความรู้ด้าน testing ผ่านคำถามที่เปิดเผยประสบการณ์กับ codebase จริง นี่คือ pattern ที่แยกแยะผู้สมัครที่เตรียมตัวมา:

"จะทดสอบฟังก์ชันที่เรียก API ภายนอกอย่างไร?"

กำหนด interface สำหรับ HTTP client, inject เข้าไปในฟังก์ชันหรือ struct และให้ mock ที่คืน response ที่กำหนดไว้ล่วงหน้า วิธีนี้หลีกเลี่ยงการเรียก network และทำให้ test เป็น deterministic

"เมื่อใดควรใช้ stub เทียบกับ mock?"

Stub คืน response ที่กำหนดไว้โดยไม่ตรวจสอบว่าถูกเรียกอย่างไร Mock ตรวจสอบว่า method เฉพาะถูกเรียกด้วย argument เฉพาะ ใช้ stub เมื่อค่าที่คืนสำคัญกว่า interaction ใช้ mock เมื่อการตรวจสอบ interaction เป็นจุดประสงค์ของ test

"Table-driven test คืออะไร และทำไม Go ถึงชอบ?"

Table-driven test กำหนด test case เป็นข้อมูลใน slice of struct ลดโค้ดซ้ำซ้อน ทำให้การเพิ่ม case เป็นเรื่องง่าย และสร้าง test output ที่อ่านง่ายด้วย t.Run pattern นี้สอดคล้องกับความชอบของ Go สำหรับโค้ดที่ชัดเจนและซ้ำๆ มากกว่า abstraction ที่ซับซ้อน

"จะจัดการกับ test fixture หรือ setup ที่หลาย test ใช้ร่วมกันอย่างไร?"

ใช้ TestMain สำหรับ setup และ teardown ระดับ package ใช้ helper function หรือ test suite (จาก testify) สำหรับ setup ร่วมระหว่าง test ที่เกี่ยวข้อง หลีกเลี่ยง global state ที่สร้าง coupling ระหว่าง test

เตรียมตัวสำหรับสัมภาษณ์ Go? โมดูล testing Go ครอบคลุม pattern เหล่านี้อย่างละเอียดพร้อมคำถามฝึกหัด

การรัน Test พร้อม Coverage และ Race Detection

คำสั่ง go test รับ flag ที่เปิดเผย code path ที่ยังไม่ได้ทดสอบและ bug ด้าน concurrency

bash
# Run tests with coverage report
go test -cover ./...

# Generate HTML coverage visualization
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html

# Detect race conditions (slower, but catches real bugs)
go test -race ./...

race detector ตรวจสอบการเข้าถึงหน่วยความจำและระบุการอ่านและเขียนพร้อมกันไปยังตัวแปรเดียวกัน การรัน test ด้วย -race ใน CI จับ bug ที่เกิดขึ้นเฉพาะที่ timing บางอย่าง

การจัดระเบียบ Test ใน Codebase ขนาดใหญ่

เมื่อโปรเจกต์เติบโต การจัดระเบียบ test ส่งผลต่อความสามารถในการบำรุงรักษา:

  • Same package test (package foo) เข้าถึงฟังก์ชันและ field ที่ไม่ได้ export ใช้สำหรับ unit test ที่ตรวจสอบพฤติกรรมภายใน
  • External package test (package foo_test) import package เหมือนโค้ดภายนอก ใช้สำหรับ integration test และตรวจสอบ public API
  • โฟลเดอร์ testdata เก็บ fixture เช่น ไฟล์ JSON, golden output หรือ seed database Go ข้ามสิ่งเหล่านี้ระหว่าง build
  • Build tag แยก unit test ออกจาก integration test ที่ต้องการระบบภายนอก รัน go test -tags=integration ./... เพื่อรวมเข้าด้วย

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับ best practice ของ Go นอกเหนือจาก testing บทความ concurrency pattern ครอบคลุม goroutine และ channel

สิ่งที่ต้องจดจำเกี่ยวกับ Go Testing

  • package testing มาพร้อมกับ Go และไม่ต้องการ dependency ภายนอก
  • Table-driven test ลดความซ้ำซ้อนและเป็น pattern ที่คาดหวังในการสัมภาษณ์
  • Interface เปิดใช้งาน dependency injection; กำหนดที่จุดใช้งาน ไม่ใช่ที่ implementation
  • gomock สร้าง mock จาก interface; testify มี assertion ที่สะอาดกว่า
  • httptest.ResponseRecorder บันทึก output ของ handler โดยไม่มี network overhead
  • flag -race ตรวจจับ bug ด้าน concurrency ที่ unit test ปกติพลาด
  • ตัวชี้วัด coverage ช่วยนำทางความสนใจแต่ไม่รับประกันความถูกต้อง

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Go เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 24 สิงหาคม 2569

แชร์

บทความที่เกี่ยวข้อง