Golang Testing

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

by affaan-mef648e01899ba3e8dc6371642deaaf64b4477775No license275K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated 4 days ago

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

Instructions onlySoftware Development
AI-generated overview

Go testing patterns covering table-driven tests, subtests, benchmarks, fuzzing, coverage and TDD workflow.

What it does
This skill provides reference guidance and code examples for writing Go tests, following a TDD red-green-refactor workflow. It covers table-driven tests, subtests and parallel subtests, test helpers, golden files, interface-based mocking, benchmarks, fuzzing, coverage measurement, and HTTP handler testing. It produces example test code and command-line invocations rather than running anything itself.
When to use it
Use it when writing new Go functions or methods, adding test coverage to existing Go code, or creating benchmarks for performance-critical code. It also fits implementing fuzz tests for input validation and following a TDD workflow in a Go project.
Requirements
Go toolchain (fuzzing requires Go 1.18+); no scripts are shipped, instructions only.

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}'

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

Source and attribution

Source:affaan-m/ECCindocs/ko-KR/skills/golang-testingat commitef648e0

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal