Golang Testing

affaan-m/ECC/docs/ko-KR/skills/golang-testing

作者 affaan-mef648e01899ba3e8dc6371642deaaf64b4477775無授權條款275K 個星標收錄於 2026年10月9日更新於 2026年10月9日儲存庫4 天前更新

테이블 주도 테스트, 서브테스트, 벤치마크, 퍼징, 테스트 커버리지를 포함한 Go 테스팅 패턴. 관용적 Go 관행과 함께 TDD 방법론을 따릅니다.

AI 產生的概覽

Go 測試模式,涵蓋表格驅動測試、子測試、基準測試、模糊測試、覆蓋率與 TDD 工作流程。

功能
此技能提供撰寫 Go 測試的參考指引與程式碼範例,遵循 TDD 的紅-綠-重構流程。內容涵蓋表格驅動測試、子測試與平行子測試、測試輔助函式、黃金檔案、以介面為基礎的模擬、基準測試、模糊測試、覆蓋率統計以及 HTTP 處理器測試。它產出範例測試程式碼與命令列用法,本身不執行任何動作。
適用情境
適用於撰寫新的 Go 函式或方法、為既有 Go 程式碼補充測試覆蓋率,或為效能關鍵的程式碼建立基準測試。也適合為輸入驗證實作模糊測試,以及在 Go 專案中遵循 TDD 工作流程。
執行需求
需要 Go 工具鏈(模糊測試需 Go 1.18 以上);不隨附指令稿,僅為說明性內容。

Go 테스팅 패턴

TDD 방법론을 따르는 신뢰할 수 있고 유지보수 가능한 테스트 작성을 위한 포괄적인 Go 테스팅 패턴.

활성화 시점

  • 새로운 Go 함수나 메서드 작성 시
  • 기존 코드에 테스트 커버리지 추가 시
  • 성능이 중요한 코드에 벤치마크 생성 시
  • 입력 유효성 검사를 위한 퍼즈 테스트 구현 시
  • Go 프로젝트에서 TDD 워크플로우 따를 시

Go에서의 TDD 워크플로우

RED-GREEN-REFACTOR 사이클

RED     → Write a failing test firstGREEN   → Write minimal code to pass the testREFACTOR → Improve code while keeping tests greenREPEAT  → Continue with next requirement

Go에서의 단계별 TDD

go
// Step 1: Define the interface/signature// calculator.gopackage calculator
func Add(a, b int) int {    panic("not implemented") // Placeholder}
// Step 2: Write failing test (RED)// calculator_test.gopackage calculator
import "testing"
func TestAdd(t *testing.T) {    got := Add(2, 3)    want := 5    if got != want {        t.Errorf("Add(2, 3) = %d; want %d", got, want)    }}
// Step 3: Run test - verify FAIL// $ go test// --- FAIL: TestAdd (0.00s)// panic: not implemented
// Step 4: Implement minimal code (GREEN)func Add(a, b int) int {    return a + b}
// Step 5: Run test - verify PASS// $ go test// PASS
// Step 6: Refactor if needed, verify tests still pass

테이블 주도 테스트

Go 테스트의 표준 패턴. 최소한의 코드로 포괄적인 커버리지를 가능하게 합니다.

go
func TestAdd(t *testing.T) {    tests := []struct {        name     string        a, b     int        expected int    }{        {"positive numbers", 2, 3, 5},        {"negative numbers", -1, -2, -3},        {"zero values", 0, 0, 0},        {"mixed signs", -1, 1, 0},        {"large numbers", 1000000, 2000000, 3000000},    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            got := Add(tt.a, tt.b)            if got != tt.expected {                t.Errorf("Add(%d, %d) = %d; want %d",                    tt.a, tt.b, got, tt.expected)            }        })    }}

에러 케이스가 있는 테이블 주도 테스트

go
func TestParseConfig(t *testing.T) {    tests := []struct {        name    string        input   string        want    *Config        wantErr bool    }{        {            name:  "valid config",            input: `{"host": "localhost", "port": 8080}`,            want:  &Config{Host: "localhost", Port: 8080},        },        {            name:    "invalid JSON",            input:   `{invalid}`,            wantErr: true,        },        {            name:    "empty input",            input:   "",            wantErr: true,        },        {            name:  "minimal config",            input: `{}`,            want:  &Config{}, // Zero value config        },    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            got, err := ParseConfig(tt.input)
            if tt.wantErr {                if err == nil {                    t.Error("expected error, got nil")                }                return            }
            if err != nil {                t.Fatalf("unexpected error: %v", err)            }
            if !reflect.DeepEqual(got, tt.want) {                t.Errorf("got %+v; want %+v", got, tt.want)            }        })    }}

서브테스트 및 서브벤치마크

관련 테스트 구성

go
func TestUser(t *testing.T) {    // Setup shared by all subtests    db := setupTestDB(t)
    t.Run("Create", func(t *testing.T) {        user := &User{Name: "Alice"}        err := db.CreateUser(user)        if err != nil {            t.Fatalf("CreateUser failed: %v", err)        }        if user.ID == "" {            t.Error("expected user ID to be set")        }    })
    t.Run("Get", func(t *testing.T) {        user, err := db.GetUser("alice-id")        if err != nil {            t.Fatalf("GetUser failed: %v", err)        }        if user.Name != "Alice" {            t.Errorf("got name %q; want %q", user.Name, "Alice")        }    })
    t.Run("Update", func(t *testing.T) {        // ...    })
    t.Run("Delete", func(t *testing.T) {        // ...    })}

병렬 서브테스트

go
func TestParallel(t *testing.T) {    tests := []struct {        name  string        input string    }{        {"case1", "input1"},        {"case2", "input2"},        {"case3", "input3"},    }
    for _, tt := range tests {        tt := tt // Capture range variable        t.Run(tt.name, func(t *testing.T) {            t.Parallel() // Run subtests in parallel            result := Process(tt.input)            // assertions...            _ = result        })    }}

테스트 헬퍼

헬퍼 함수

go
func setupTestDB(t *testing.T) *sql.DB {    t.Helper() // Marks this as a helper function
    db, err := sql.Open("sqlite3", ":memory:")    if err != nil {        t.Fatalf("failed to open database: %v", err)    }
    // Cleanup when test finishes    t.Cleanup(func() {        db.Close()    })
    // Run migrations    if _, err := db.Exec(schema); err != nil {        t.Fatalf("failed to create schema: %v", err)    }
    return db}
func assertNoError(t *testing.T, err error) {    t.Helper()    if err != nil {        t.Fatalf("unexpected error: %v", err)    }}
func assertEqual[T comparable](t *testing.T, got, want T) {    t.Helper()    if got != want {        t.Errorf("got %v; want %v", got, want)    }}

임시 파일 및 디렉터리

go
func TestFileProcessing(t *testing.T) {    // Create temp directory - automatically cleaned up    tmpDir := t.TempDir()
    // Create test file    testFile := filepath.Join(tmpDir, "test.txt")    err := os.WriteFile(testFile, []byte("test content"), 0644)    if err != nil {        t.Fatalf("failed to create test file: %v", err)    }
    // Run test    result, err := ProcessFile(testFile)    if err != nil {        t.Fatalf("ProcessFile failed: %v", err)    }
    // Assert...    _ = result}

골든 파일

testdata/에 저장된 예상 출력 파일에 대한 테스트.

go
var update = flag.Bool("update", false, "update golden files")
func TestRender(t *testing.T) {    tests := []struct {        name  string        input Template    }{        {"simple", Template{Name: "test"}},        {"complex", Template{Name: "test", Items: []string{"a", "b"}}},    }
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            got := Render(tt.input)
            golden := filepath.Join("testdata", tt.name+".golden")
            if *update {                // Update golden file: go test -update                err := os.WriteFile(golden, got, 0644)                if err != nil {                    t.Fatalf("failed to update golden file: %v", err)                }            }
            want, err := os.ReadFile(golden)            if err != nil {                t.Fatalf("failed to read golden file: %v", err)            }
            if !bytes.Equal(got, want) {                t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want)            }        })    }}

인터페이스를 사용한 모킹

인터페이스 기반 모킹

go
// Define interface for dependenciestype UserRepository interface {    GetUser(id string) (*User, error)    SaveUser(user *User) error}
// Production implementationtype PostgresUserRepository struct {    db *sql.DB}
func (r *PostgresUserRepository) GetUser(id string) (*User, error) {    // Real database query}
// Mock implementation for teststype MockUserRepository struct {    GetUserFunc  func(id string) (*User, error)    SaveUserFunc func(user *User) error}
func (m *MockUserRepository) GetUser(id string) (*User, error) {    return m.GetUserFunc(id)}
func (m *MockUserRepository) SaveUser(user *User) error {    return m.SaveUserFunc(user)}
// Test using mockfunc TestUserService(t *testing.T) {    mock := &MockUserRepository{        GetUserFunc: func(id string) (*User, error) {            if id == "123" {                return &User{ID: "123", Name: "Alice"}, nil            }            return nil, ErrNotFound        },    }
    service := NewUserService(mock)
    user, err := service.GetUserProfile("123")    if err != nil {        t.Fatalf("unexpected error: %v", err)    }    if user.Name != "Alice" {        t.Errorf("got name %q; want %q", user.Name, "Alice")    }}

벤치마크

기본 벤치마크

go
func BenchmarkProcess(b *testing.B) {    data := generateTestData(1000)    b.ResetTimer() // Don't count setup time
    for i := 0; i < b.N; i++ {        Process(data)    }}
// Run: go test -bench=BenchmarkProcess -benchmem// Output: BenchmarkProcess-8   10000   105234 ns/op   4096 B/op   10 allocs/op

다양한 크기의 벤치마크

go
func BenchmarkSort(b *testing.B) {    sizes := []int{100, 1000, 10000, 100000}
    for _, size := range sizes {        b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {            data := generateRandomSlice(size)            b.ResetTimer()
            for i := 0; i < b.N; i++ {                // Make a copy to avoid sorting already sorted data                tmp := make([]int, len(data))                copy(tmp, data)                sort.Ints(tmp)            }        })    }}

메모리 할당 벤치마크

go
func BenchmarkStringConcat(b *testing.B) {    parts := []string{"hello", "world", "foo", "bar", "baz"}
    b.Run("plus", func(b *testing.B) {        for i := 0; i < b.N; i++ {            var s string            for _, p := range parts {                s += p            }            _ = s        }    })
    b.Run("builder", func(b *testing.B) {        for i := 0; i < b.N; i++ {            var sb strings.Builder            for _, p := range parts {                sb.WriteString(p)            }            _ = sb.String()        }    })
    b.Run("join", func(b *testing.B) {        for i := 0; i < b.N; i++ {            _ = strings.Join(parts, "")        }    })}

퍼징 (Go 1.18+)

기본 퍼즈 테스트

go
func FuzzParseJSON(f *testing.F) {    // Add seed corpus    f.Add(`{"name": "test"}`)    f.Add(`{"count": 123}`)    f.Add(`[]`)    f.Add(`""`)
    f.Fuzz(func(t *testing.T, input string) {        var result map[string]interface{}        err := json.Unmarshal([]byte(input), &result)
        if err != nil {            // Invalid JSON is expected for random input            return        }
        // If parsing succeeded, re-encoding should work        _, err = json.Marshal(result)        if err != nil {            t.Errorf("Marshal failed after successful Unmarshal: %v", err)        }    })}
// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s

다중 입력 퍼즈 테스트

go
func FuzzCompare(f *testing.F) {    f.Add("hello", "world")    f.Add("", "")    f.Add("abc", "abc")
    f.Fuzz(func(t *testing.T, a, b string) {        result := Compare(a, b)
        // Property: Compare(a, a) should always equal 0        if a == b && result != 0 {            t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result)        }
        // Property: Compare(a, b) and Compare(b, a) should have opposite signs        reverse := Compare(b, a)        if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) {            if result != 0 || reverse != 0 {                t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent",                    a, b, result, b, a, reverse)            }        }    })}

테스트 커버리지

커버리지 실행

bash
# Basic coveragego test -cover ./...
# Generate coverage profilego test -coverprofile=coverage.out ./...
# View coverage in browsergo tool cover -html=coverage.out
# View coverage by functiongo tool cover -func=coverage.out
# Coverage with race detectiongo test -race -coverprofile=coverage.out ./...

커버리지 목표

코드 유형목표
핵심 비즈니스 로직100%
공개 API90%+
일반 코드80%+
생성된 코드제외

생성된 코드를 커버리지에서 제외

go
//go:generate mockgen -source=interface.go -destination=mock_interface.go
// In coverage profile, exclude with build tags:// go test -cover -tags=!generate ./...

HTTP 핸들러 테스팅

go
func TestHealthHandler(t *testing.T) {    // Create request    req := httptest.NewRequest(http.MethodGet, "/health", nil)    w := httptest.NewRecorder()
    // Call handler    HealthHandler(w, req)
    // Check response    resp := w.Result()    defer resp.Body.Close()
    if resp.StatusCode != http.StatusOK {        t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK)    }
    body, _ := io.ReadAll(resp.Body)    if string(body) != "OK" {        t.Errorf("got body %q; want %q", body, "OK")    }}
func TestAPIHandler(t *testing.T) {    tests := []struct {        name       string        method     string        path       string        body       string        wantStatus int        wantBody   string    }{        {            name:       "get user",            method:     http.MethodGet,            path:       "/users/123",            wantStatus: http.StatusOK,            wantBody:   `{"id":"123","name":"Alice"}`,        },        {            name:       "not found",            method:     http.MethodGet,            path:       "/users/999",            wantStatus: http.StatusNotFound,        },        {            name:       "create user",            method:     http.MethodPost,            path:       "/users",            body:       `{"name":"Bob"}`,            wantStatus: http.StatusCreated,        },    }
    handler := NewAPIHandler()
    for _, tt := range tests {        t.Run(tt.name, func(t *testing.T) {            var body io.Reader            if tt.body != "" {                body = strings.NewReader(tt.body)            }
            req := httptest.NewRequest(tt.method, tt.path, body)            req.Header.Set("Content-Type", "application/json")            w := httptest.NewRecorder()
            handler.ServeHTTP(w, req)
            if w.Code != tt.wantStatus {                t.Errorf("got status %d; want %d", w.Code, tt.wantStatus)            }
            if tt.wantBody != "" && w.Body.String() != tt.wantBody {                t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody)            }        })    }}

테스팅 명령어

bash
# Run all testsgo test ./...
# Run tests with verbose outputgo test -v ./...
# Run specific testgo test -run TestAdd ./...
# Run tests matching patterngo test -run "TestUser/Create" ./...
# Run tests with race detectorgo test -race ./...
# Run tests with coveragego test -cover -coverprofile=coverage.out ./...
# Run short tests onlygo test -short ./...
# Run tests with timeoutgo test -timeout 30s ./...
# Run benchmarksgo test -bench=. -benchmem ./...
# Run fuzzinggo test -fuzz=FuzzParse -fuzztime=30s ./...
# Count test runs (for flaky test detection)go test -count=10 ./...

모범 사례

해야 할 것:

  • 테스트를 먼저 작성 (TDD)
  • 포괄적인 커버리지를 위해 테이블 주도 테스트 사용
  • 구현이 아닌 동작을 테스트
  • 헬퍼 함수에서 t.Helper() 사용
  • 독립적인 테스트에 t.Parallel() 사용
  • t.Cleanup()으로 리소스 정리
  • 시나리오를 설명하는 의미 있는 테스트 이름 사용

하지 말아야 할 것:

  • 비공개 함수를 직접 테스트 (공개 API를 통해 테스트)
  • 테스트에서 time.Sleep() 사용 (채널이나 조건 사용)
  • 불안정한 테스트 무시 (수정하거나 제거)
  • 모든 것을 모킹 (가능하면 통합 테스트 선호)
  • 에러 경로 테스트 생략

CI/CD 통합

yaml
# GitHub Actions exampletest:  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 -race -coverprofile=coverage.out ./...
    - name: Check coverage      run: |        go tool cover -func=coverage.out | grep total | awk '{print $3}' | \        awk -F'%' '{if ($1 < 80) exit 1}'

기억하세요: 테스트는 문서입니다. 코드가 어떻게 사용되어야 하는지를 보여줍니다. 명확하게 작성하고 최신 상태로 유지하세요.

來源與署名

來源:affaan-m/ECC位於docs/ko-KR/skills/golang-testing提交ef648e0

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架