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

การทดสอบใน 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 สำหรับรายงานความล้มเหลว
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 ทั่วอุตสาหกรรม
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 ได้
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 ที่บันทึกการเรียกและคืนค่าที่กำหนดไว้
# Generate mocks for the Repository interface
mockgen -source=service.go -destination=mocks/repository_mock.go -package=mocksการใช้ mock ที่สร้างขึ้นใน test:
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 ทันที
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
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
# 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ผู้ก่อตั้ง SharpSkill
เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่
อัปเดตเมื่อ 24 สิงหาคม 2569
แชร์
บทความที่เกี่ยวข้อง

Profiling และ Benchmarking Go 2026: pprof, trace และคำถามสัมภาษณ์
คู่มือ profiling Go ด้วย pprof และ runtime/trace อย่างครอบคลุม เทคนิควิเคราะห์ CPU, หน่วยความจำ และ goroutine เพื่อเพิ่มประสิทธิภาพและเตรียมตัวสัมภาษณ์

Interface Go ขั้นสูงในปี 2026: การประกอบ Type Assertion และคำถามสัมภาษณ์
คู่มือเชิงลึกเกี่ยวกับ interface Go สมัยใหม่: การประกอบ interface, type assertion ที่ปลอดภัย, generic แบบอ้างอิงตนเอง และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Go

Go Context Package 2026: การยกเลิก, Timeout และคำถามสัมภาษณ์งาน
เชี่ยวชาญ package context ของ Go สำหรับการยกเลิก, timeout และ request-scoped values คู่มือฉบับสมบูรณ์พร้อมตัวอย่างโค้ดและคำถามสัมภาษณ์