본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout
TechProjectsBooksAbout
© 2026 Kreath. All rights reserved.
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 9장: 테스트 전략 - 단위, 통합, 벤치마크
2026년 8월 28일·프로그래밍·

9장: 테스트 전략 - 단위, 통합, 벤치마크

Go의 testing 패키지를 중심으로 테이블 기반 테스트, testify, httptest, 목(Mock) 생성, testcontainers-go 통합 테스트, 벤치마크, 퍼징 테스트 전략을 다룹니다.

14분1,053자11개 섹션
concurrencyperformancedesign-patternstestinginfrastructure
공유
go-backend9 / 11
1234567891011
이전8장: 미들웨어, 인증, API 설계다음10장: gRPC와 마이크로서비스 통신

학습 목표

  • Go의 testing 패키지와 테이블 기반 테스트 패턴을 활용할 수 있다
  • testify를 사용하여 간결한 단언(Assertion)을 작성할 수 있다
  • httptest로 HTTP 핸들러를 테스트할 수 있다
  • testcontainers-go로 실제 데이터베이스 통합 테스트를 구성할 수 있다
  • 벤치마크와 퍼징 테스트를 작성할 수 있다

Go 테스트의 기본

testing 패키지

Go는 테스트를 언어와 도구 체인 수준에서 지원합니다. 별도의 테스트 프레임워크 설치 없이 go test 명령으로 실행할 수 있습니다.

user_service_test.go
go
package service
 
import "testing"
 
func TestUserService_ValidateEmail(t *testing.T) {
    svc := NewUserService(nil)
 
    // 유효한 이메일
    err := svc.ValidateEmail("user@example.com")
    if err != nil {
        t.Errorf("유효한 이메일인데 에러 발생: %v", err)
    }
 
    // 잘못된 이메일
    err = svc.ValidateEmail("invalid-email")
    if err == nil {
        t.Error("잘못된 이메일인데 에러가 발생하지 않음")
    }
}
테스트 실행 명령어
bash
# 현재 패키지 테스트
go test ./...
 
# 상세 출력
go test -v ./...
 
# 특정 테스트만 실행
go test -run TestUserService_ValidateEmail ./service/...
 
# 커버리지 측정
go test -cover ./...
 
# 커버리지 리포트 생성
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Info

Go 테스트 파일은 _test.go 접미사를 가지며, 빌드 시 자동으로 제외됩니다. 테스트 함수는 반드시 Test로 시작하고 *testing.T 매개변수를 받아야 합니다.


테이블 기반 테스트

테이블 기반 테스트(Table-driven Test)는 Go 커뮤니티에서 가장 권장되는 테스트 패턴입니다. 여러 테스트 케이스를 구조체 슬라이스로 정의하고 루프에서 실행합니다.

table-driven-test.go
go
func TestCalculateDiscount(t *testing.T) {
    tests := []struct {
        name     string
        price    float64
        quantity int
        want     float64
        wantErr  bool
    }{
        {
            name:     "일반 주문 -- 할인 없음",
            price:    10000,
            quantity: 1,
            want:     10000,
        },
        {
            name:     "대량 주문 -- 10% 할인",
            price:    10000,
            quantity: 10,
            want:     90000,
        },
        {
            name:     "VIP 대량 주문 -- 20% 할인",
            price:    10000,
            quantity: 100,
            want:     800000,
        },
        {
            name:     "음수 수량 -- 에러",
            price:    10000,
            quantity: -1,
            wantErr:  true,
        },
        {
            name:     "가격 0 -- 에러",
            price:    0,
            quantity: 1,
            wantErr:  true,
        },
    }
 
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := CalculateDiscount(tt.price, tt.quantity)
 
            if tt.wantErr {
                if err == nil {
                    t.Error("에러를 기대했으나 발생하지 않음")
                }
                return
            }
 
            if err != nil {
                t.Fatalf("예상하지 못한 에러: %v", err)
            }
            if got != tt.want {
                t.Errorf("got %v, want %v", got, tt.want)
            }
        })
    }
}

t.Run을 사용하면 각 서브테스트가 독립적으로 실행되며, go test -run "TestCalculateDiscount/대량" 처럼 특정 서브테스트만 실행할 수도 있습니다.


testify

testify는 Go에서 가장 많이 사용되는 테스트 보조 라이브러리로, 간결한 단언(Assertion)과 목(Mock) 기능을 제공합니다.

assert와 require

testify-assert.go
go
import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)
 
func TestUserService_CreateUser(t *testing.T) {
    svc := setupTestService(t)
 
    user, err := svc.CreateUser(ctx, CreateUserRequest{
        Name:  "Kreath",
        Email: "kreath@example.com",
    })
 
    // require -- 실패 시 즉시 테스트 중단
    require.NoError(t, err)
    require.NotNil(t, user)
 
    // assert -- 실패해도 나머지 단언 계속 실행
    assert.Equal(t, "Kreath", user.Name)
    assert.Equal(t, "kreath@example.com", user.Email)
    assert.NotZero(t, user.ID)
    assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Second)
}
Tip

require는 실패 시 테스트를 즉시 중단하고, assert는 실패해도 나머지 단언을 계속 실행합니다. 후속 단언이 의미 없어지는 경우(예: nil 체크) require를 사용하고, 독립적인 검증에는 assert를 사용하세요.

testify/mock

testify-mock.go
go
import "github.com/stretchr/testify/mock"
 
// 목 객체 정의
type MockUserRepository struct {
    mock.Mock
}
 
func (m *MockUserRepository) FindByID(ctx context.Context, id int64) (*User, error) {
    args := m.Called(ctx, id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*User), args.Error(1)
}
 
func (m *MockUserRepository) Save(ctx context.Context, user *User) error {
    args := m.Called(ctx, user)
    return args.Error(0)
}
 
// 테스트에서 목 사용
func TestUserService_GetUser(t *testing.T) {
    mockRepo := new(MockUserRepository)
    svc := NewUserService(mockRepo)
 
    expectedUser := &User{ID: 1, Name: "Kreath"}
    mockRepo.On("FindByID", mock.Anything, int64(1)).Return(expectedUser, nil)
 
    user, err := svc.GetUser(context.Background(), 1)
 
    require.NoError(t, err)
    assert.Equal(t, expectedUser, user)
    mockRepo.AssertExpectations(t)
}
 
// 에러 시나리오 테스트
func TestUserService_GetUser_NotFound(t *testing.T) {
    mockRepo := new(MockUserRepository)
    svc := NewUserService(mockRepo)
 
    mockRepo.On("FindByID", mock.Anything, int64(999)).
        Return(nil, ErrNotFound)
 
    user, err := svc.GetUser(context.Background(), 999)
 
    assert.Nil(t, user)
    assert.ErrorIs(t, err, ErrNotFound)
}

httptest -- HTTP 핸들러 테스트

net/http/httptest 패키지는 HTTP 핸들러를 실제 서버 없이 테스트할 수 있게 합니다.

httptest-example.go
go
import "net/http/httptest"
 
func TestGetUserHandler(t *testing.T) {
    // 목 서비스 설정
    mockSvc := new(MockUserService)
    handler := NewUserHandler(mockSvc)
 
    expectedUser := &User{ID: 1, Name: "Kreath", Email: "kreath@example.com"}
    mockSvc.On("GetUser", mock.Anything, int64(1)).Return(expectedUser, nil)
 
    // 요청 생성
    req := httptest.NewRequest("GET", "/api/v1/users/1", nil)
    req.SetPathValue("id", "1") // Go 1.22+ 경로 파라미터
 
    // 응답 레코더
    rec := httptest.NewRecorder()
 
    // 핸들러 실행
    handler.GetUser(rec, req)
 
    // 검증
    assert.Equal(t, http.StatusOK, rec.Code)
 
    var response User
    err := json.NewDecoder(rec.Body).Decode(&response)
    require.NoError(t, err)
    assert.Equal(t, "Kreath", response.Name)
}
 
func TestGetUserHandler_NotFound(t *testing.T) {
    mockSvc := new(MockUserService)
    handler := NewUserHandler(mockSvc)
 
    mockSvc.On("GetUser", mock.Anything, int64(999)).
        Return(nil, ErrNotFound)
 
    req := httptest.NewRequest("GET", "/api/v1/users/999", nil)
    req.SetPathValue("id", "999")
    rec := httptest.NewRecorder()
 
    handler.GetUser(rec, req)
 
    assert.Equal(t, http.StatusNotFound, rec.Code)
}

통합 수준 HTTP 테스트

실제 라우터와 미들웨어 스택을 포함한 통합 테스트입니다.

integration-http-test.go
go
func TestAPI_Integration(t *testing.T) {
    // 전체 앱 설정 (테스트 DB 사용)
    app := setupTestApp(t)
 
    // 테스트 서버 생성
    server := httptest.NewServer(app.Router())
    defer server.Close()
 
    client := server.Client()
 
    // 회원가입
    body := `{"name": "Test", "email": "test@example.com", "password": "secure123"}`
    resp, err := client.Post(server.URL+"/api/v1/auth/register",
        "application/json", strings.NewReader(body))
    require.NoError(t, err)
    assert.Equal(t, http.StatusCreated, resp.StatusCode)
 
    // 로그인
    loginBody := `{"email": "test@example.com", "password": "secure123"}`
    resp, err = client.Post(server.URL+"/api/v1/auth/login",
        "application/json", strings.NewReader(loginBody))
    require.NoError(t, err)
 
    var loginResp LoginResponse
    json.NewDecoder(resp.Body).Decode(&loginResp)
    assert.NotEmpty(t, loginResp.AccessToken)
}

testcontainers-go -- 통합 테스트

testcontainers-go는 테스트에서 Docker 컨테이너를 프로그래밍 방식으로 관리합니다. 실제 PostgreSQL, Redis 등을 테스트 환경에서 사용할 수 있습니다.

testcontainers-example.go
go
import (
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/testcontainers/testcontainers-go/wait"
)
 
func setupTestDB(t *testing.T) *pgxpool.Pool {
    t.Helper()
    ctx := context.Background()
 
    pgContainer, err := postgres.Run(ctx,
        "postgres:16-alpine",
        postgres.WithDatabase("testdb"),
        postgres.WithUsername("testuser"),
        postgres.WithPassword("testpass"),
        testcontainers.WithWaitStrategy(
            wait.ForLog("database system is ready to accept connections").
                WithOccurrence(2).
                WithStartupTimeout(30*time.Second),
        ),
    )
    require.NoError(t, err)
 
    t.Cleanup(func() {
        require.NoError(t, pgContainer.Terminate(ctx))
    })
 
    connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
    require.NoError(t, err)
 
    pool, err := pgxpool.New(ctx, connStr)
    require.NoError(t, err)
 
    // 마이그레이션 실행
    runMigrations(t, connStr)
 
    return pool
}
 
func TestUserRepo_Integration(t *testing.T) {
    if testing.Short() {
        t.Skip("통합 테스트 건너뜀 (-short 플래그)")
    }
 
    pool := setupTestDB(t)
    repo := NewUserRepo(pool)
    ctx := context.Background()
 
    // 생성
    user := &User{Name: "Kreath", Email: "kreath@test.com"}
    err := repo.Create(ctx, user)
    require.NoError(t, err)
    assert.NotZero(t, user.ID)
 
    // 조회
    found, err := repo.FindByID(ctx, user.ID)
    require.NoError(t, err)
    assert.Equal(t, "Kreath", found.Name)
}
Info

testing.Short() 플래그를 활용하면 CI 환경에서는 전체 테스트를, 로컬 개발 시에는 빠른 단위 테스트만 실행할 수 있습니다. go test -short ./...으로 Docker 의존성이 있는 통합 테스트를 건너뛸 수 있습니다.


벤치마크 테스트

기본 벤치마크

benchmark-test.go
go
func BenchmarkJSONMarshal(b *testing.B) {
    user := User{
        ID:    1,
        Name:  "Kreath",
        Email: "kreath@example.com",
    }
 
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        json.Marshal(user)
    }
}
 
func BenchmarkJSONMarshal_Large(b *testing.B) {
    users := make([]User, 1000)
    for i := range users {
        users[i] = User{ID: int64(i), Name: "User", Email: "user@test.com"}
    }
 
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        json.Marshal(users)
    }
}
벤치마크 실행
bash
# 벤치마크 실행
go test -bench=. -benchmem ./...
 
# 특정 벤치마크만
go test -bench=BenchmarkJSONMarshal -benchmem ./...
 
# 결과 예시
# BenchmarkJSONMarshal-8      5000000     234 ns/op    128 B/op    2 allocs/op

비교 벤치마크

comparison-benchmark.go
go
func BenchmarkMapAccess(b *testing.B) {
    m := make(map[string]int)
    for i := 0; i < 1000; i++ {
        m[fmt.Sprintf("key-%d", i)] = i
    }
 
    b.Run("map", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = m["key-500"]
        }
    })
 
    b.Run("sync.Map", func(b *testing.B) {
        var sm sync.Map
        for k, v := range m {
            sm.Store(k, v)
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
            sm.Load("key-500")
        }
    })
}

퍼징 테스트

Go 1.18에서 도입된 퍼징(Fuzzing)은 무작위 입력을 생성하여 예상하지 못한 버그를 발견합니다.

fuzz-test.go
go
func FuzzParseUserInput(f *testing.F) {
    // 시드 코퍼스 추가
    f.Add("kreath@example.com")
    f.Add("invalid")
    f.Add("")
    f.Add("a@b.c")
    f.Add(strings.Repeat("a", 1000) + "@test.com")
 
    f.Fuzz(func(t *testing.T, input string) {
        result, err := ParseUserInput(input)
        if err != nil {
            return // 에러는 허용
        }
        // 성공했다면 결과가 유효해야 함
        if result.Email != "" && !strings.Contains(result.Email, "@") {
            t.Errorf("유효하지 않은 이메일이 통과됨: %s", result.Email)
        }
    })
}
퍼징 실행
bash
# 30초간 퍼징 실행
go test -fuzz=FuzzParseUserInput -fuzztime=30s ./...
Tip

퍼징은 파서, 직렬화/역직렬화, 유효성 검사 로직에서 특히 효과적입니다. 개발자가 미처 생각하지 못한 엣지 케이스를 자동으로 발견해줍니다.


테스트 전략 피라미드

테스트 유형별 가이드
text
단위 테스트 (70%)
- 비즈니스 로직, 유틸리티 함수
- 의존성은 목으로 대체
- 실행 시간: 밀리초 단위
 
통합 테스트 (20%)
- 데이터베이스, 외부 서비스 연동
- testcontainers 또는 테스트 DB 사용
- 실행 시간: 초 단위
 
E2E 테스트 (10%)
- 전체 API 흐름 검증
- httptest.NewServer 활용
- 실행 시간: 수초 단위

정리

이번 장에서 살펴본 핵심 내용을 정리합니다.

  • 테이블 기반 테스트는 Go에서 가장 권장되는 테스트 패턴으로, 여러 케이스를 구조화하여 관리합니다
  • testify의 assert/require와 mock은 테스트 코드를 간결하고 표현력 있게 만듭니다
  • httptest로 실제 서버 없이 HTTP 핸들러를 테스트할 수 있습니다
  • testcontainers-go로 실제 데이터베이스를 사용한 통합 테스트를 자동화할 수 있습니다
  • 벤치마크와 퍼징은 성능 최적화와 엣지 케이스 발견에 효과적입니다

다음 장 미리보기

10장에서는 gRPC와 마이크로서비스 통신을 다룹니다. Protocol Buffers, gRPC의 유니어리/스트리밍 통신, 인터셉터, gRPC-Gateway를 통한 REST 변환, 헬스 체킹과 서비스 디스커버리를 살펴봅니다.

이 글이 도움이 되셨나요?

관련 글

프로그래밍

10장: gRPC와 마이크로서비스 통신

Protocol Buffers 정의부터 Go에서의 gRPC 서버/클라이언트 구현, 유니어리/스트리밍 통신, 인터셉터, gRPC-Gateway를 통한 REST 변환, 헬스 체킹과 서비스 디스커버리를 다룹니다.

2026년 8월 30일·12분
프로그래밍

8장: 미들웨어, 인증, API 설계

Go 백엔드의 미들웨어 패턴, JWT 인증 구현, CORS 설정, 레이트 리미팅, 요청 유효성 검사, OpenAPI/Swagger 문서 생성, log/slog를 활용한 구조화된 로깅을 다룹니다.

2026년 8월 25일·11분
프로그래밍

11장: 실전 프로젝트 - Go 백엔드 API 서버 구축

시리즈의 모든 내용을 종합하여 Chi 기반 REST API 서버, PostgreSQL(pgx), JWT 인증, 미들웨어 스택, 구조화된 로깅, 테스트, Docker 빌드, CI/CD 파이프라인을 갖춘 완전한 백엔드를 구축합니다.

2026년 9월 2일·16분
이전 글8장: 미들웨어, 인증, API 설계
다음 글10장: gRPC와 마이크로서비스 통신

댓글

목차

약 14분 남음
  • 학습 목표
  • Go 테스트의 기본
    • testing 패키지
  • 테이블 기반 테스트
  • testify
    • assert와 require
    • testify/mock
  • httptest -- HTTP 핸들러 테스트
    • 통합 수준 HTTP 테스트
  • testcontainers-go -- 통합 테스트
  • 벤치마크 테스트
    • 기본 벤치마크
    • 비교 벤치마크
  • 퍼징 테스트
  • 테스트 전략 피라미드
  • 정리
  • 다음 장 미리보기