Skip to content

필사 모드: Go 테스팅 완전 가이드: 단위 테스트, Mock, Stub 완벽 정복

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

소개

소프트웨어 품질은 테스트로 증명됩니다. Go는 언어 자체에 강력한 테스팅 도구를 내장하고 있으며, `testing` 패키지와 `go test` 명령어만으로도 프로덕션 수준의 테스트를 작성할 수 있습니다.

이 가이드에서는 기본 단위 테스트부터 Mock/Stub을 활용한 의존성 분리, 통합 테스트까지 실전에서 바로 사용 가능한 패턴을 다룹니다.

1. Go 테스팅 기본

1.1 testing 패키지

Go의 테스트는 `_test.go` 파일에 작성하며, 테스트 함수는 `Test` 접두사로 시작합니다.

// calculator.go

package calculator

func Add(a, b int) int { return a + b }

func Sub(a, b int) int { return a - b }

func Mul(a, b int) int { return a * b }

func Div(a, b float64) (float64, error) {

if b == 0 {

return 0, fmt.Errorf("division by zero")

}

return a / b, nil

}

// calculator_test.go

package calculator

"testing"

)

func TestAdd(t *testing.T) {

result := Add(2, 3)

expected := 5

if result != expected {

t.Errorf("Add(2, 3) = %d; want %d", result, expected)

}

}

// t.Fatal: 이후 테스트 코드 실행 중단

func TestDiv(t *testing.T) {

result, err := Div(10, 2)

if err != nil {

t.Fatalf("예상치 못한 에러: %v", err)

}

if result != 5.0 {

t.Errorf("Div(10, 2) = %f; want 5.0", result)

}

// 0으로 나누기 에러 테스트

_, err = Div(10, 0)

if err == nil {

t.Error("0으로 나누기 시 에러를 반환해야 합니다")

}

}

// t.Helper(): 헬퍼 함수에서 호출 시 오류 위치가 헬퍼 내부가 아닌 호출 지점으로 표시

func assertEqual(t *testing.T, got, want int) {

t.Helper()

if got != want {

t.Errorf("got %d, want %d", got, want)

}

}

func TestMul(t *testing.T) {

assertEqual(t, Mul(3, 4), 12)

assertEqual(t, Mul(0, 100), 0)

assertEqual(t, Mul(-2, 5), -10)

}

1.2 테스트 실행

현재 패키지 테스트

go test

모든 패키지 테스트

go test ./...

상세 출력

go test -v

특정 테스트 함수만 실행 (정규식)

go test -run TestAdd

go test -run TestDiv

테스트 커버리지

go test -cover

go test -coverprofile=coverage.out

go tool cover -html=coverage.out # 브라우저로 커버리지 확인

go tool cover -func=coverage.out # 함수별 커버리지

타임아웃 설정

go test -timeout 30s

레이스 컨디션 감지

go test -race

캐시 무시

go test -count=1 ./...

1.3 TestMain

package mypackage_test

"fmt"

"os"

"testing"

)

var testDB *Database

// TestMain: 테스트 실행 전/후 설정 및 해제

func TestMain(m *testing.M) {

// 설정 (setup)

fmt.Println("테스트 환경 설정 시작")

testDB = setupTestDatabase()

// 테스트 실행

code := m.Run()

// 해제 (teardown)

testDB.Close()

fmt.Println("테스트 환경 해제 완료")

os.Exit(code)

}

2. 테이블 주도 테스트 (Table-Driven Tests)

Go에서 가장 관용적인 테스트 패턴입니다.

package calculator_test

"fmt"

"testing"

"github.com/example/calculator"

)

func TestAdd_TableDriven(t *testing.T) {

tests := []struct {

name string

a, b int

expected int

}{

{"양수+양수", 2, 3, 5},

{"양수+음수", 5, -3, 2},

{"음수+음수", -4, -6, -10},

{"제로케이스", 0, 0, 0},

{"대수", 1000000, 2000000, 3000000},

}

for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {

result := calculator.Add(tt.a, tt.b)

if result != tt.expected {

t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)

}

})

}

}

// 에러 케이스를 포함한 테이블 테스트

func TestDiv_TableDriven(t *testing.T) {

tests := []struct {

name string

a, b float64

expected float64

expectErr bool

}{

{"정상 나누기", 10, 2, 5.0, false},

{"소수 결과", 7, 2, 3.5, false},

{"0으로 나누기", 10, 0, 0, true},

{"음수", -10, 2, -5.0, false},

}

for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {

result, err := calculator.Div(tt.a, tt.b)

if tt.expectErr {

if err == nil {

t.Error("에러를 기대했지만 nil이 반환됨")

}

return

}

if err != nil {

t.Fatalf("예상치 못한 에러: %v", err)

}

if result != tt.expected {

t.Errorf("Div(%.1f, %.1f) = %.1f; want %.1f",

tt.a, tt.b, result, tt.expected)

}

})

}

}

// 서브테스트 setup/teardown 패턴

func TestUserService_TableDriven(t *testing.T) {

tests := []struct {

name string

userID int

wantErr bool

}{

{"유효한 유저", 1, false},

{"존재하지 않는 유저", 999, true},

{"음수 ID", -1, true},

}

for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {

// 서브테스트별 setup

svc := NewTestUserService()

defer svc.Cleanup()

user, err := svc.GetUser(tt.userID)

if (err != nil) != tt.wantErr {

t.Errorf("GetUser(%d) error = %v, wantErr %v",

tt.userID, err, tt.wantErr)

return

}

if !tt.wantErr {

fmt.Printf("유저: %+v\n", user)

}

})

}

}

3. testify 라이브러리

`testify`는 Go에서 가장 많이 사용되는 테스트 라이브러리입니다.

go get github.com/stretchr/testify

3.1 assert vs require

package mypackage_test

"testing"

"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/require"

)

func TestWithTestify(t *testing.T) {

result := Add(2, 3)

// assert: 실패해도 테스트 계속 실행

assert.Equal(t, 5, result, "2+3은 5여야 합니다")

assert.NotEqual(t, 6, result)

assert.True(t, result > 0)

assert.False(t, result < 0)

// require: 실패하면 테스트 즉시 중단

require.NotNil(t, result)

require.Equal(t, 5, result)

}

func TestCollections(t *testing.T) {

nums := []int{1, 2, 3, 4, 5}

assert.Contains(t, nums, 3)

assert.NotContains(t, nums, 10)

assert.Len(t, nums, 5)

assert.Empty(t, []int{})

assert.NotEmpty(t, nums)

m := map[string]int{"a": 1, "b": 2}

assert.Equal(t, 1, m["a"])

}

func TestErrors(t *testing.T) {

_, err := Div(10, 0)

assert.Error(t, err)

assert.EqualError(t, err, "division by zero")

result, err := Div(10, 2)

assert.NoError(t, err)

assert.Equal(t, 5.0, result)

}

func TestPanic(t *testing.T) {

assert.Panics(t, func() {

panic("test panic")

})

assert.NotPanics(t, func() {

_ = Add(1, 2)

})

}

3.2 assert.JSONEq와 HTTP 어서션

package mypackage_test

"net/http"

"net/http/httptest"

"testing"

"github.com/stretchr/testify/assert"

)

func TestJSONResponse(t *testing.T) {

handler := func(w http.ResponseWriter, r *http.Request) {

w.Header().Set("Content-Type", "application/json")

w.WriteHeader(http.StatusOK)

w.Write([]byte(`{"name":"Alice","age":30}`))

}

req := httptest.NewRequest(http.MethodGet, "/user", nil)

rec := httptest.NewRecorder()

handler(rec, req)

assert.Equal(t, http.StatusOK, rec.Code)

assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))

// JSON 내용 비교 (키 순서 무관)

expected := `{"age": 30, "name": "Alice"}`

assert.JSONEq(t, expected, rec.Body.String())

}

3.3 testify/suite

package mypackage_test

"testing"

"github.com/stretchr/testify/assert"

"github.com/stretchr/testify/suite"

)

type UserServiceSuite struct {

suite.Suite

service *UserService

db *TestDB

}

// 스위트 시작 전 한 번 실행

func (s *UserServiceSuite) SetupSuite() {

s.db = NewTestDB()

}

// 각 테스트 전 실행

func (s *UserServiceSuite) SetupTest() {

s.service = NewUserService(s.db)

s.db.Seed()

}

// 각 테스트 후 실행

func (s *UserServiceSuite) TearDownTest() {

s.db.Clean()

}

// 스위트 종료 후 한 번 실행

func (s *UserServiceSuite) TearDownSuite() {

s.db.Close()

}

func (s *UserServiceSuite) TestGetUser() {

user, err := s.service.GetUser(1)

s.NoError(err)

s.Equal("Alice", user.Name)

}

func (s *UserServiceSuite) TestCreateUser() {

user, err := s.service.CreateUser("Bob", "bob@example.com")

s.NoError(err)

s.NotZero(user.ID)

s.Equal("Bob", user.Name)

}

func (s *UserServiceSuite) TestDeleteUser_NotFound() {

err := s.service.DeleteUser(9999)

s.Error(err)

assert.Contains(s.T(), err.Error(), "not found")

}

// Go 테스트 러너에 스위트 등록

func TestUserServiceSuite(t *testing.T) {

suite.Run(t, new(UserServiceSuite))

}

4. 인터페이스를 활용한 Mock

4.1 인터페이스 기반 의존성 주입

// user_repository.go

package user

type User struct {

ID int

Name string

Email string

}

// 인터페이스 정의 (의존성 주입의 핵심)

type Repository interface {

FindByID(ctx context.Context, id int) (*User, error)

Save(ctx context.Context, user *User) error

Delete(ctx context.Context, id int) error

List(ctx context.Context) ([]*User, error)

}

type EmailSender interface {

Send(to, subject, body string) error

}

// 서비스는 인터페이스에 의존 (구체적 구현에 의존하지 않음)

type Service struct {

repo Repository

email EmailSender

}

func NewService(repo Repository, email EmailSender) *Service {

return &Service{repo: repo, email: email}

}

func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {

return s.repo.FindByID(ctx, id)

}

func (s *Service) CreateUser(ctx context.Context, name, email string) (*User, error) {

user := &User{Name: name, Email: email}

if err := s.repo.Save(ctx, user); err != nil {

return nil, fmt.Errorf("saving user: %w", err)

}

// 이메일 발송 (실패해도 유저 생성은 성공)

_ = s.email.Send(email, "환영합니다", "회원가입을 축하합니다!")

return user, nil

}

4.2 수동 Mock 구현

// mock_repository.go (테스트 파일에서 직접 정의 가능)

package user_test

"context"

"fmt"

"github.com/example/user"

)

type MockRepository struct {

users map[int]*user.User

saveErr error

findErr error

}

func NewMockRepository() *MockRepository {

return &MockRepository{users: make(map[int]*user.User)}

}

func (m *MockRepository) FindByID(ctx context.Context, id int) (*user.User, error) {

if m.findErr != nil {

return nil, m.findErr

}

u, ok := m.users[id]

if !ok {

return nil, fmt.Errorf("user %d not found", id)

}

return u, nil

}

func (m *MockRepository) Save(ctx context.Context, u *user.User) error {

if m.saveErr != nil {

return m.saveErr

}

if u.ID == 0 {

u.ID = len(m.users) + 1

}

m.users[u.ID] = u

return nil

}

func (m *MockRepository) Delete(ctx context.Context, id int) error {

delete(m.users, id)

return nil

}

func (m *MockRepository) List(ctx context.Context) ([]*user.User, error) {

users := make([]*user.User, 0, len(m.users))

for _, u := range m.users {

users = append(users, u)

}

return users, nil

}

type MockEmailSender struct {

SentEmails []struct{ To, Subject, Body string }

SendErr error

}

func (m *MockEmailSender) Send(to, subject, body string) error {

if m.SendErr != nil {

return m.SendErr

}

m.SentEmails = append(m.SentEmails, struct{ To, Subject, Body string }{to, subject, body})

return nil

}

5. gomock 사용법

5.1 gomock 설치와 코드 생성

go install go.uber.org/mock/mockgen@latest

go get go.uber.org/mock/gomock

인터페이스 파일에 `go:generate` 지시어 추가:

// repository.go

package user

//go:generate mockgen -source=repository.go -destination=mocks/mock_repository.go -package=mocks

type Repository interface {

FindByID(ctx context.Context, id int) (*User, error)

Save(ctx context.Context, user *User) error

Delete(ctx context.Context, id int) error

List(ctx context.Context) ([]*User, error)

}

Mock 생성

go generate ./...

5.2 gomock 테스트 작성

package user_test

"context"

"testing"

"github.com/example/user"

"github.com/example/user/mocks"

"go.uber.org/mock/gomock"

"github.com/stretchr/testify/assert"

)

func TestGetUser_WithGomock(t *testing.T) {

ctrl := gomock.NewController(t)

defer ctrl.Finish()

mockRepo := mocks.NewMockRepository(ctrl)

mockEmail := mocks.NewMockEmailSender(ctrl)

expectedUser := &user.User{ID: 1, Name: "Alice", Email: "alice@example.com"}

// EXPECT: 메서드 호출 기대 설정

mockRepo.EXPECT().

FindByID(gomock.Any(), 1).

Return(expectedUser, nil).

Times(1)

svc := user.NewService(mockRepo, mockEmail)

got, err := svc.GetUser(context.Background(), 1)

assert.NoError(t, err)

assert.Equal(t, expectedUser, got)

}

func TestCreateUser_WithGomock(t *testing.T) {

ctrl := gomock.NewController(t)

defer ctrl.Finish()

mockRepo := mocks.NewMockRepository(ctrl)

mockEmail := mocks.NewMockEmailSender(ctrl)

// Save가 정확히 1번 호출되어야 함

mockRepo.EXPECT().

Save(gomock.Any(), gomock.Any()).

DoAndReturn(func(ctx context.Context, u *user.User) error {

u.ID = 42 // ID 할당 시뮬레이션

return nil

}).

Times(1)

// Send는 0번 이상 호출될 수 있음

mockEmail.EXPECT().

Send(gomock.Any(), gomock.Any(), gomock.Any()).

Return(nil).

AnyTimes()

svc := user.NewService(mockRepo, mockEmail)

got, err := svc.CreateUser(context.Background(), "Bob", "bob@example.com")

assert.NoError(t, err)

assert.Equal(t, 42, got.ID)

assert.Equal(t, "Bob", got.Name)

}

// 특정 인수 매처 사용

func TestFindByID_SpecificArgs(t *testing.T) {

ctrl := gomock.NewController(t)

defer ctrl.Finish()

mockRepo := mocks.NewMockRepository(ctrl)

// 정확한 인수 매칭

mockRepo.EXPECT().

FindByID(gomock.Any(), gomock.Eq(5)).

Return(nil, fmt.Errorf("not found")).

Times(1)

// 커스텀 매처

mockRepo.EXPECT().

FindByID(gomock.Any(), gomock.Not(gomock.Eq(0))).

Return(&user.User{ID: 10}, nil).

AnyTimes()

}

6. testify/mock 사용법

6.1 testify/mock으로 Mock 구현

package user_test

"context"

"testing"

"github.com/example/user"

"github.com/stretchr/testify/mock"

"github.com/stretchr/testify/assert"

)

// testify/mock을 임베딩한 Mock 구조체

type MockUserRepo struct {

mock.Mock

}

func (m *MockUserRepo) FindByID(ctx context.Context, id int) (*user.User, error) {

args := m.Called(ctx, id)

if args.Get(0) == nil {

return nil, args.Error(1)

}

return args.Get(0).(*user.User), args.Error(1)

}

func (m *MockUserRepo) Save(ctx context.Context, u *user.User) error {

args := m.Called(ctx, u)

return args.Error(0)

}

func (m *MockUserRepo) Delete(ctx context.Context, id int) error {

args := m.Called(ctx, id)

return args.Error(0)

}

func (m *MockUserRepo) List(ctx context.Context) ([]*user.User, error) {

args := m.Called(ctx)

return args.Get(0).([]*user.User), args.Error(1)

}

func TestGetUser_Testify(t *testing.T) {

mockRepo := new(MockUserRepo)

mockEmail := new(MockEmailSender)

expectedUser := &user.User{ID: 1, Name: "Alice"}

// On으로 메서드 동작 설정

mockRepo.On("FindByID", mock.Anything, 1).

Return(expectedUser, nil)

svc := user.NewService(mockRepo, mockEmail)

got, err := svc.GetUser(context.Background(), 1)

assert.NoError(t, err)

assert.Equal(t, expectedUser, got)

// 모든 기대가 충족되었는지 확인

mockRepo.AssertExpectations(t)

}

func TestCreateUser_Testify(t *testing.T) {

mockRepo := new(MockUserRepo)

mockEmail := new(MockEmailSender)

// mock.MatchedBy로 커스텀 매처 사용

mockRepo.On("Save", mock.Anything, mock.MatchedBy(func(u *user.User) bool {

return u.Name == "Bob"

})).Return(nil)

mockEmail.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(nil)

svc := user.NewService(mockRepo, mockEmail)

_, err := svc.CreateUser(context.Background(), "Bob", "bob@example.com")

assert.NoError(t, err)

mockRepo.AssertNumberOfCalls(t, "Save", 1)

mockEmail.AssertNumberOfCalls(t, "Send", 1)

}

7. HTTP Mock 서버 (httptest)

7.1 httptest.NewRecorder

package handler_test

"bytes"

"encoding/json"

"net/http"

"net/http/httptest"

"testing"

"github.com/stretchr/testify/assert"

)

func TestUserHandler_GetAll(t *testing.T) {

// 핸들러 생성

store := NewInMemoryStore()

store.Add(&User{ID: 1, Name: "Alice"})

store.Add(&User{ID: 2, Name: "Bob"})

handler := NewUserHandler(store)

// 요청 생성

req := httptest.NewRequest(http.MethodGet, "/users", nil)

rec := httptest.NewRecorder()

// 핸들러 직접 호출

handler.ServeHTTP(rec, req)

// 응답 검증

assert.Equal(t, http.StatusOK, rec.Code)

assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))

var users []User

err := json.NewDecoder(rec.Body).Decode(&users)

assert.NoError(t, err)

assert.Len(t, users, 2)

}

func TestUserHandler_Create(t *testing.T) {

store := NewInMemoryStore()

handler := NewUserHandler(store)

body := map[string]string{

"name": "Charlie",

"email": "charlie@example.com",

}

jsonBody, _ := json.Marshal(body)

req := httptest.NewRequest(http.MethodPost, "/users",

bytes.NewBuffer(jsonBody))

req.Header.Set("Content-Type", "application/json")

rec := httptest.NewRecorder()

handler.ServeHTTP(rec, req)

assert.Equal(t, http.StatusCreated, rec.Code)

var created User

json.NewDecoder(rec.Body).Decode(&created)

assert.Equal(t, "Charlie", created.Name)

assert.NotZero(t, created.ID)

}

7.2 httptest.NewServer (외부 API 모킹)

package client_test

"encoding/json"

"net/http"

"net/http/httptest"

"testing"

"github.com/stretchr/testify/assert"

)

func TestHTTPClient_GetUser(t *testing.T) {

// 외부 서버를 모킹하는 테스트 서버

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

// 요청 검증

assert.Equal(t, "/api/users/1", r.URL.Path)

assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))

// 응답 반환

w.Header().Set("Content-Type", "application/json")

json.NewEncoder(w).Encode(map[string]interface{}{

"id": 1,

"name": "Alice",

"email": "alice@example.com",

})

}))

defer ts.Close()

// 테스트 서버 URL로 클라이언트 생성

client := NewUserClient(ts.URL, "test-token")

user, err := client.GetUser(1)

assert.NoError(t, err)

assert.Equal(t, "Alice", user.Name)

}

// 에러 시나리오 테스트

func TestHTTPClient_ServerError(t *testing.T) {

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

http.Error(w, "Internal Server Error", http.StatusInternalServerError)

}))

defer ts.Close()

client := NewUserClient(ts.URL, "test-token")

_, err := client.GetUser(1)

assert.Error(t, err)

}

// 라우터 설정이 있는 테스트 서버

func TestHTTPClient_MultipleEndpoints(t *testing.T) {

mux := http.NewServeMux()

mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {

json.NewEncoder(w).Encode([]User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}})

})

mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {

json.NewEncoder(w).Encode(map[string]string{"status": "ok"})

})

ts := httptest.NewServer(mux)

defer ts.Close()

client := NewUserClient(ts.URL, "token")

users, err := client.ListUsers()

assert.NoError(t, err)

assert.Len(t, users, 2)

}

8. Stub 패턴

8.1 인터페이스 기반 Stub

// 시간 Stub: time.Now()를 교체 가능하게 만들기

type Clock interface {

Now() time.Time

}

type RealClock struct{}

func (r RealClock) Now() time.Time { return time.Now() }

type FakeClock struct {

fakeTime time.Time

}

func (f FakeClock) Now() time.Time { return f.fakeTime }

// 파일시스템 Stub

type FileSystem interface {

ReadFile(name string) ([]byte, error)

WriteFile(name string, data []byte, perm os.FileMode) error

Exists(name string) bool

}

type OSFileSystem struct{}

func (f OSFileSystem) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }

func (f OSFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error {

return os.WriteFile(name, data, perm)

}

func (f OSFileSystem) Exists(name string) bool {

_, err := os.Stat(name)

return !os.IsNotExist(err)

}

// 인메모리 파일시스템 Stub

type MemoryFileSystem struct {

files map[string][]byte

}

func NewMemoryFileSystem() *MemoryFileSystem {

return &MemoryFileSystem{files: make(map[string][]byte)}

}

func (m *MemoryFileSystem) ReadFile(name string) ([]byte, error) {

data, ok := m.files[name]

if !ok {

return nil, fmt.Errorf("file not found: %s", name)

}

return data, nil

}

func (m *MemoryFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error {

m.files[name] = data

return nil

}

func (m *MemoryFileSystem) Exists(name string) bool {

_, ok := m.files[name]

return ok

}

// Stub을 활용한 서비스 테스트

type ConfigService struct {

fs FileSystem

clock Clock

}

func (s *ConfigService) LoadConfig(path string) (*Config, error) {

data, err := s.fs.ReadFile(path)

if err != nil {

return nil, err

}

var cfg Config

if err := json.Unmarshal(data, &cfg); err != nil {

return nil, err

}

cfg.LoadedAt = s.clock.Now()

return &cfg, nil

}

8.2 데이터베이스 Stub (sqlmock)

package repository_test

"testing"

"regexp"

"github.com/DATA-DOG/go-sqlmock"

"github.com/stretchr/testify/assert"

)

func TestUserRepository_FindByID(t *testing.T) {

db, mock, err := sqlmock.New()

if err != nil {

t.Fatalf("sqlmock 생성 실패: %v", err)

}

defer db.Close()

// 쿼리 기대 설정

rows := sqlmock.NewRows([]string{"id", "name", "email"}).

AddRow(1, "Alice", "alice@example.com")

mock.ExpectQuery(regexp.QuoteMeta("SELECT id, name, email FROM users WHERE id = ?")).

WithArgs(1).

WillReturnRows(rows)

repo := NewSQLUserRepository(db)

user, err := repo.FindByID(context.Background(), 1)

assert.NoError(t, err)

assert.Equal(t, "Alice", user.Name)

// 모든 기대가 충족되었는지 확인

assert.NoError(t, mock.ExpectationsWereMet())

}

func TestUserRepository_Save(t *testing.T) {

db, mock, _ := sqlmock.New()

defer db.Close()

mock.ExpectExec(regexp.QuoteMeta(

"INSERT INTO users (name, email) VALUES (?, ?)")).

WithArgs("Bob", "bob@example.com").

WillReturnResult(sqlmock.NewResult(42, 1))

repo := NewSQLUserRepository(db)

user := &User{Name: "Bob", Email: "bob@example.com"}

err := repo.Save(context.Background(), user)

assert.NoError(t, err)

assert.Equal(t, 42, user.ID)

assert.NoError(t, mock.ExpectationsWereMet())

}

9. 모의 서버 (Mock Server) 만들기

// mockserver/server.go

package mockserver

"encoding/json"

"net/http"

"net/http/httptest"

"sync"

"testing"

)

type RequestRecord struct {

Method string

Path string

Headers http.Header

Body []byte

}

type MockResponse struct {

Status int

Headers map[string]string

Body interface{}

}

type Route struct {

Method string

Path string

Response MockResponse

}

type MockServer struct {

t *testing.T

server *httptest.Server

routes []Route

requests []RequestRecord

mu sync.Mutex

}

func New(t *testing.T) *MockServer {

ms := &MockServer{t: t}

mux := http.NewServeMux()

mux.HandleFunc("/", ms.handleRequest)

ms.server = httptest.NewServer(mux)

t.Cleanup(ms.server.Close)

return ms

}

func (ms *MockServer) handleRequest(w http.ResponseWriter, r *http.Request) {

ms.mu.Lock()

defer ms.mu.Unlock()

// 요청 기록

body, _ := io.ReadAll(r.Body)

ms.requests = append(ms.requests, RequestRecord{

Method: r.Method,

Path: r.URL.Path,

Headers: r.Header.Clone(),

Body: body,

})

// 라우트 매칭

for _, route := range ms.routes {

if route.Method == r.Method && route.Path == r.URL.Path {

for k, v := range route.Response.Headers {

w.Header().Set(k, v)

}

if _, ok := route.Response.Headers["Content-Type"]; !ok {

w.Header().Set("Content-Type", "application/json")

}

w.WriteHeader(route.Response.Status)

if route.Response.Body != nil {

json.NewEncoder(w).Encode(route.Response.Body)

}

return

}

}

// 매칭되는 라우트 없음

ms.t.Errorf("예상치 못한 요청: %s %s", r.Method, r.URL.Path)

http.Error(w, "Not Found", http.StatusNotFound)

}

func (ms *MockServer) AddRoute(method, path string, resp MockResponse) *MockServer {

ms.mu.Lock()

defer ms.mu.Unlock()

ms.routes = append(ms.routes, Route{Method: method, Path: path, Response: resp})

return ms

}

func (ms *MockServer) URL() string {

return ms.server.URL

}

func (ms *MockServer) Requests() []RequestRecord {

ms.mu.Lock()

defer ms.mu.Unlock()

return append([]RequestRecord{}, ms.requests...)

}

func (ms *MockServer) AssertRequestCount(t *testing.T, method, path string, count int) {

t.Helper()

actual := 0

for _, r := range ms.Requests() {

if r.Method == method && r.Path == path {

actual++

}

}

if actual != count {

t.Errorf("%s %s: 요청 횟수 = %d, want %d", method, path, actual, count)

}

}

// 사용 예시

func TestWithMockServer(t *testing.T) {

ms := New(t)

ms.AddRoute("GET", "/api/users", MockResponse{

Status: 200,

Body: []map[string]interface{}{{"id": 1, "name": "Alice"}},

})

ms.AddRoute("POST", "/api/users", MockResponse{

Status: 201,

Body: map[string]interface{}{"id": 2, "name": "Bob"},

})

client := NewAPIClient(ms.URL())

users, err := client.GetUsers()

assert.NoError(t, err)

assert.Len(t, users, 1)

ms.AssertRequestCount(t, "GET", "/api/users", 1)

}

10. Benchmark 테스트

package calculator_test

// Benchmark: go test -bench=. 으로 실행

func BenchmarkAdd(b *testing.B) {

for i := 0; i < b.N; i++ {

Add(100, 200)

}

}

func BenchmarkFibonacci(b *testing.B) {

sizes := []int{10, 20, 30}

for _, n := range sizes {

n := n

b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {

for i := 0; i < b.N; i++ {

Fibonacci(n)

}

})

}

}

// 메모리 할당 추적

func BenchmarkStringConcat(b *testing.B) {

b.ReportAllocs()

for i := 0; i < b.N; i++ {

var s string

for j := 0; j < 100; j++ {

s += "x"

}

_ = s

}

}

func BenchmarkStringBuilder(b *testing.B) {

b.ReportAllocs()

for i := 0; i < b.N; i++ {

var sb strings.Builder

for j := 0; j < 100; j++ {

sb.WriteString("x")

}

_ = sb.String()

}

}

11. 퀴즈

`assert.Equal`과 `require.Equal`의 차이는 무엇인가요? 언제 어떤 것을 사용해야 하나요?

**정답**:

- `assert.Equal`: 실패해도 테스트를 계속 실행합니다. `t.Error()`를 내부적으로 호출합니다.

- `require.Equal`: 실패하면 테스트를 즉시 중단합니다. `t.FailNow()`를 내부적으로 호출합니다.

**설명**:

`require`는 이후 코드가 전제 조건에 의존할 때 사용합니다:

func TestUserCreation(t *testing.T) {

user, err := CreateUser("Alice", "alice@example.com")

require.NoError(t, err) // 에러가 있으면 user가 nil이므로 이후 코드가 패닉

require.NotNil(t, user)

assert.Equal(t, "Alice", user.Name) // 이후 검증은 assert

assert.NotEmpty(t, user.ID)

}

`assert`는 독립적으로 검증할 수 있는 여러 항목을 모두 확인할 때 사용합니다.

다음 테이블 주도 테스트의 버그는 무엇인가요?

tests := []struct {

name string

input int

want int

}{

{"double 2", 2, 4},

{"double 5", 5, 10},

{"double 10", 10, 20},

}

for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {

t.Parallel()

result := Double(tt.input)

assert.Equal(t, tt.want, result)

})

}

**정답**: `t.Parallel()` 사용 시 클로저가 루프 변수 `tt`를 공유합니다.

**설명**: `t.Parallel()`을 호출하면 테스트가 병렬로 실행되는데, 이때 클로저 내에서 `tt`를 참조하면 모든 고루틴이 같은 변수를 공유합니다. 루프가 빠르게 끝나면 마지막 값만 남아 모든 테스트가 같은 입력으로 실행될 수 있습니다.

수정 방법:

for _, tt := range tests {

tt := tt // 루프 변수를 로컬 변수로 캡처

t.Run(tt.name, func(t *testing.T) {

t.Parallel()

result := Double(tt.input)

assert.Equal(t, tt.want, result)

})

}

Go 1.22 이후에는 루프 변수 동작이 변경되어 이 문제가 없어졌습니다.

Mock과 Stub의 차이점을 설명하고, 언제 각각을 사용해야 하나요?

**정답**:

- **Stub**: 미리 정해진 응답을 반환하는 단순한 대체물. 상태 검증(state verification)에 사용.

- **Mock**: 특정 메서드가 특정 인수로 호출되었는지 검증하는 객체. 행위 검증(behavior verification)에 사용.

**설명**:

Stub 예시 — 고정 응답 반환:

type StubUserRepo struct{}

func (s StubUserRepo) FindByID(ctx context.Context, id int) (*User, error) {

return &User{ID: id, Name: "Test User"}, nil

}

Mock 예시 — 호출 검증:

mockRepo.EXPECT().FindByID(ctx, 1).Return(user, nil).Times(1)

// 테스트 후 FindByID가 정확히 1번 호출되었는지 자동 검증

규칙: 테스트가 메서드 호출 여부/횟수/인수를 검증해야 한다면 Mock, 단순히 특정 응답이 필요하다면 Stub을 사용합니다.

`go test -race`는 무엇을 감지하고, 언제 사용해야 하나요?

**정답**: 레이스 컨디션(Race Condition)을 감지합니다. 여러 고루틴이 동기화 없이 같은 메모리 위치에 접근할 때를 탐지합니다.

**설명**:

// 이 코드는 -race 플래그로 감지됨

var counter int

func TestRaceCondition(t *testing.T) {

var wg sync.WaitGroup

for i := 0; i < 1000; i++ {

wg.Add(1)

go func() {

defer wg.Done()

counter++ // DATA RACE: 동기화 없이 쓰기

}()

}

wg.Wait()

}

`-race` 플래그는 런타임 오버헤드가 크므로 (5~10배 느려짐) CI/CD 파이프라인에서 실행하거나, 동시성 코드를 작성할 때 주기적으로 사용하는 것이 좋습니다.

테스트 커버리지 100%가 코드에 버그가 없음을 보장하나요? 커버리지 측정 시 주의해야 할 점은?

**정답**: 아니요. 커버리지 100%는 버그가 없음을 보장하지 않습니다.

**설명**: 커버리지는 코드 실행 여부를 측정하지, 올바른 동작을 검증하지 않습니다.

func Divide(a, b int) int {

return a / b

}

// 커버리지 100%이지만 중요한 케이스를 놓침

func TestDivide(t *testing.T) {

assert.Equal(t, 5, Divide(10, 2))

// b=0 케이스를 테스트하지 않아 패닉 발생 가능

}

커버리지보다 중요한 것들:

1. 경계값 테스트 (0, -1, max값 등)

2. 에러 케이스 테스트

3. 동시성 테스트

4. 통합 테스트

커버리지는 테스트하지 않은 코드를 찾는 도구이지, 테스트 품질을 보장하지 않습니다.

현재 단락 (1/955)

소프트웨어 품질은 테스트로 증명됩니다. Go는 언어 자체에 강력한 테스팅 도구를 내장하고 있으며, `testing` 패키지와 `go test` 명령어만으로도 프로덕션 수준의 테스트...

작성 글자: 0원문 글자: 22,060작성 단락: 0/955