Go의 테스트 주도 개발(TDD) 방법론 완벽 가이드 🚀

2025년 최신 트렌드와 함께하는 Go TDD 여행
안녕하세요, Go 개발자 여러분! 오늘은 2025년 2월 28일 기준으로 최신 정보를 담아 Go 언어에서의 테스트 주도 개발(TDD)에 대해 함께 알아볼게요. 혹시 TDD라는 말만 들어도 "아 또 뭔가 어려운 거 아냐?" 하고 겁먹으셨나요? ㅋㅋㅋ 걱정 노노! 오늘은 진짜 쉽고 재밌게 Go의 TDD를 파헤쳐 볼 거예요! 😎
요즘 개발 트렌드를 보면 TDD가 선택이 아닌 필수가 되어가고 있어요. 특히 Go 언어는 기본적으로 테스트 도구를 내장하고 있어서 TDD 적용하기 넘모넘모 좋은 언어랍니다! 재능넷에서도 Go 개발자 수요가 계속 늘고 있는데, TDD 역량을 갖춘 개발자는 특히 몸값이 훅훅 올라가고 있다는 사실! 아시나요? 👀
💡 알고 가기: 테스트 주도 개발(TDD)은 코드를 작성하기 전에 먼저 테스트를 작성하는 개발 방법론이에요. Red(실패하는 테스트 작성) → Green(테스트 통과하는 코드 작성) → Refactor(코드 개선) 사이클을 반복하며 개발하는 방식이죠!
1. Go에서 TDD가 왜 중요할까요? 🤔
일단 솔직히 말해볼게요. 코드 먼저 작성하고 나중에 테스트 작성하는 게 더 편하지 않나요? ㅋㅋㅋ 저도 처음엔 그랬어요! 근데 진짜 큰 프로젝트 해보면 나중에 테스트 작성은... 현실은 영원한 나중이 되어버리는 경우가 많더라고요. 😅
Go에서 TDD를 적용하면 얻을 수 있는 이점들을 살펴볼까요?
- 버그 감소: 테스트를 먼저 작성하면 요구사항을 명확히 이해하게 되고, 이는 버그를 줄이는 데 엄청난 도움이 돼요. 진짜 "앗 이게 왜 안 돼?" 하는 상황이 확실히 줄어들어요!
- 리팩토링 자신감: 테스트가 있으면 코드를 마음껏 개선할 수 있어요. "이거 고치면 다른 거 터지는 거 아냐?"라는 두려움 없이 말이죠!
- 문서화 효과: 테스트 코드는 실행 가능한 문서와 같아요. 다른 개발자들이 여러분의 코드를 이해하는 데 큰 도움이 됩니다.
- 설계 개선: 테스트를 먼저 작성하면 자연스럽게 모듈화되고 결합도가 낮은 코드를 작성하게 돼요. 이건 진짜 개이득! 👍
- 개발 속도 향상: 처음엔 느릴 수 있지만, 장기적으로는 디버깅 시간이 줄어들어 전체 개발 속도가 빨라져요.
🔍 실제 사례: 제 친구는 재능넷에서 Go 백엔드 개발자로 활동하는데, TDD를 적용한 후 버그 리포트가 무려 60% 감소했대요! 게다가 코드 리뷰 시간도 확 줄었다고 하네요. 진짜 "갓고언어" 인정? ㄹㅇ 인정! 😆
2. Go 테스팅 기본기 다지기 📚
Go는 테스트를 위한 기본 패키지인 testing
을 제공해요. 이게 바로 Go가 TDD에 찰떡인 이유 중 하나죠! 기본 문법부터 살펴볼게요.
2.1 기본 테스트 작성하기
Go에서 테스트 파일은 *_test.go 형식으로 이름을 지어야 해요. 그리고 테스트 함수는 Test로 시작해야 하고, *testing.T 매개변수를 받아야 합니다.
간단한 예제로 살펴볼까요?
// math.go
package math
func Add(a, b int) int {
return a + b
}
이제 위 함수를 테스트하는 코드를 작성해볼게요.
// math_test.go
package math
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)
}
}
테스트를 실행하려면 터미널에서 다음 명령어를 입력하면 돼요:
go test
이렇게 기본적인 테스트를 작성하고 실행하는 방법을 알아봤어요. 근데 이게 다가 아니에요! Go의 테스팅 도구는 훨씬 더 다양한 기능을 제공한답니다. 😉
2.2 테이블 주도 테스트(Table-Driven Tests)
Go에서는 테이블 주도 테스트라는 패턴을 많이 사용해요. 여러 테스트 케이스를 한 번에 처리할 수 있어서 정말 유용하답니다!
func TestAdd_TableDriven(t *testing.T) {
// 테스트 케이스 테이블 정의
tests := []struct {
name string
a, b int
expected int
}{
{"양수 더하기", 2, 3, 5},
{"음수 더하기", -1, -2, -3},
{"양수와 음수", -5, 5, 0},
}
// 각 테스트 케이스 실행
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := Add(tc.a, tc.b)
if got != tc.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
이렇게 하면 한 번에 여러 테스트 케이스를 실행할 수 있어요. 테스트 케이스 추가도 쉽고, 코드 중복도 줄일 수 있죠. 진짜 개발자들이 좋아하는 패턴이에요! 👨💻
2.3 서브테스트와 병렬 테스트
Go 1.7부터는 t.Run()
메서드를 사용해 서브테스트를 실행할 수 있어요. 이를 통해 테스트를 더 구조적으로 관리할 수 있죠. 또한 t.Parallel()
을 사용하면 테스트를 병렬로 실행할 수 있어요.
func TestMath(t *testing.T) {
t.Run("Add", func(t *testing.T) {
t.Parallel() // 이 테스트를 병렬로 실행
if Add(2, 3) != 5 {
t.Error("Add 함수 오류")
}
})
t.Run("Subtract", func(t *testing.T) {
t.Parallel() // 이 테스트를 병렬로 실행
if Subtract(5, 3) != 2 {
t.Error("Subtract 함수 오류")
}
})
}
병렬 테스트는 테스트 실행 시간을 크게 단축시킬 수 있어요. 특히 대규모 프로젝트에서는 정말 큰 차이를 만들어낸답니다! 🚀
💡 꿀팁: 병렬 테스트를 사용할 때는 공유 자원에 주의해야 해요! 테스트 간에 상태를 공유하면 예상치 못한 결과가 발생할 수 있어요. 각 테스트가 독립적으로 실행될 수 있도록 설계하는 것이 중요합니다.
3. Go에서 TDD 실전 적용하기 🛠️
이제 Go에서 TDD를 실제로 적용하는 방법을 단계별로 알아볼게요. TDD의 핵심은 Red-Green-Refactor 사이클이에요.
3.1 Red: 실패하는 테스트 작성하기
TDD의 첫 번째 단계는 실패하는 테스트를 작성하는 거예요. 이 단계에서는 아직 구현되지 않은 기능에 대한 테스트를 작성합니다.
예를 들어, 사용자 정보를 저장하고 조회하는 서비스를 만든다고 가정해볼게요.
// user_service_test.go
package user
import "testing"
func TestUserService_GetUser(t *testing.T) {
// 1. 테스트를 위한 준비
service := NewUserService()
// 2. 테스트 실행
user, err := service.GetUser(1)
// 3. 결과 확인
if err != nil {
t.Errorf("에러가 발생하지 않아야 함: %v", err)
}
if user.ID != 1 {
t.Errorf("사용자 ID가 1이어야 함, 실제: %d", user.ID)
}
if user.Name != "Gopher" {
t.Errorf("사용자 이름이 'Gopher'여야 함, 실제: %s", user.Name)
}
}
이 테스트는 아직 UserService
나 GetUser
메서드가 구현되지 않았기 때문에 실패할 거예요. 그게 바로 우리가 원하는 거죠! 🎯
테스트를 실행하면 다음과 같은 오류가 발생할 거예요:
./user_service_test.go:6:9: undefined: NewUserService
3.2 Green: 테스트를 통과하는 가장 간단한 코드 작성하기
이제 테스트를 통과하는 최소한의 코드를 작성할 차례예요. 이 단계에서는 코드의 품질보다는 테스트 통과에 집중해요.
// user_service.go
package user
type User struct {
ID int
Name string
}
type UserService struct{}
func NewUserService() *UserService {
return &UserService{}
}
func (s *UserService) GetUser(id int) (User, error) {
// 지금은 하드코딩된 값을 반환
return User{ID: 1, Name: "Gopher"}, nil
}
이 코드는 매우 단순하지만, 테스트를 통과하기에는 충분해요. 실제로는 데이터베이스에서 사용자 정보를 가져와야 하겠지만, TDD에서는 작은 단계로 점진적으로 개발하는 것이 중요해요.
테스트를 다시 실행해보면:
go test
PASS
ok github.com/yourusername/project/user 0.007s
짜잔! 테스트가 통과했어요! 🎉
3.3 Refactor: 코드 개선하기
이제 코드가 테스트를 통과했으니, 코드를 개선할 차례예요. 이 단계에서는 코드의 품질을 향상시키고, 중복을 제거하고, 가독성을 높이는 작업을 해요.
현재 코드는 하드코딩된 값을 반환하고 있어요. 실제 환경에서는 이렇게 동작하면 안 되겠죠? 데이터베이스나 다른 저장소에서 사용자 정보를 가져오도록 개선해볼게요.
// user_service.go
package user
import "errors"
type User struct {
ID int
Name string
}
type UserRepository interface {
FindByID(id int) (User, error)
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUser(id int) (User, error) {
if id <= 0 {
return User{}, errors.New("유효하지 않은 사용자 ID")
}
return s.repo.FindByID(id)
}
이제 테스트 코드도 수정해야 해요:
// user_service_test.go
package user
import "testing"
// 테스트용 레포지토리 구현
type mockUserRepository struct{}
func (m *mockUserRepository) FindByID(id int) (User, error) {
return User{ID: 1, Name: "Gopher"}, nil
}
func TestUserService_GetUser(t *testing.T) {
// 1. 테스트를 위한 준비
repo := &mockUserRepository{}
service := NewUserService(repo)
// 2. 테스트 실행
user, err := service.GetUser(1)
// 3. 결과 확인
if err != nil {
t.Errorf("에러가 발생하지 않아야 함: %v", err)
}
if user.ID != 1 {
t.Errorf("사용자 ID가 1이어야 함, 실제: %d", user.ID)
}
if user.Name != "Gopher" {
t.Errorf("사용자 이름이 'Gopher'여야 함, 실제: %s", user.Name)
}
}
이렇게 코드를 리팩토링한 후에도 테스트가 여전히 통과하는지 확인해야 해요:
go test
PASS
ok github.com/yourusername/project/user 0.007s
모든 테스트가 통과했네요! 이제 우리는 더 나은 설계를 가진 코드를 갖게 되었어요. 의존성 주입을 통해 테스트 가능성도 높아졌고, 유효성 검사도 추가되었죠. 👏
🔑 핵심 포인트: TDD는 단순히 테스트를 먼저 작성하는 것이 아니라, 테스트-구현-리팩토링의 짧은 사이클을 반복하는 개발 방법론이에요. 이 과정을 통해 요구사항에 정확히 맞는 코드를 작성하고, 지속적으로 코드 품질을 개선할 수 있어요.
4. Go 테스트의 고급 기능 활용하기 🚀
이제 Go 테스트의 더 고급 기능들을 살펴볼게요. 이런 기능들을 활용하면 더 효과적인 TDD를 실천할 수 있어요!
4.1 테스트 커버리지 측정
Go는 테스트 커버리지를 쉽게 측정할 수 있는 도구를 제공해요. 커버리지는 코드의 어느 부분이 테스트되었는지를 보여주는 지표예요.
go test -cover
더 자세한 정보를 보려면 다음 명령어를 사용할 수 있어요:
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
이 명령어는 브라우저에서 볼 수 있는 HTML 보고서를 생성해요. 테스트된 코드는 녹색으로, 테스트되지 않은 코드는 빨간색으로 표시돼요. 진짜 직관적이라 넘모 좋음! 👀
4.2 벤치마크 테스트
Go는 성능 테스트를 위한 벤치마크 기능도 제공해요. 벤치마크 함수는 Benchmark
로 시작하고 *testing.B
매개변수를 받아요.
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
벤치마크를 실행하려면:
go test -bench=.
결과는 다음과 같이 나타날 거예요:
goos: darwin
goarch: amd64
BenchmarkAdd-8 2000000000 0.33 ns/op
PASS
ok github.com/yourusername/project/math 0.700s
이렇게 함수의 성능을 측정할 수 있어요. 코드 변경 후 성능이 어떻게 변하는지 추적하는 데 정말 유용하답니다! 🏎️
4.3 테스트 헬퍼 함수
테스트 코드에서 반복되는 작업은 헬퍼 함수로 추출하는 것이 좋아요. Go에서는 t.Helper()
를 사용해 헬퍼 함수를 표시할 수 있어요.
func assertNoError(t *testing.T, err error) {
t.Helper() // 이 함수가 헬퍼 함수임을 표시
if err != nil {
t.Fatalf("예상치 못한 에러 발생: %v", err)
}
}
func TestSomething(t *testing.T) {
result, err := SomeFunction()
assertNoError(t, err) // 헬퍼 함수 사용
// 나머지 테스트 코드...
}
t.Helper()
를 사용하면 오류가 발생했을 때 실제 오류가 발생한 라인 번호가 표시돼요. 이렇게 하면 디버깅이 훨씬 쉬워진답니다! 👌
4.4 테스트 픽스처와 셋업/티어다운
테스트에 필요한 데이터나 환경을 설정하는 것을 픽스처라고 해요. Go에서는 TestMain
함수를 사용해 테스트 전체의 셋업과 티어다운을 관리할 수 있어요.
func TestMain(m *testing.M) {
// 테스트 전 셋업
fmt.Println("테스트 시작 전 셋업...")
setupTestDatabase()
// 모든 테스트 실행
exitCode := m.Run()
// 테스트 후 정리
fmt.Println("테스트 완료 후 정리...")
cleanupTestDatabase()
// 종료 코드와 함께 프로그램 종료
os.Exit(exitCode)
}
개별 테스트의 셋업과 티어다운은 다음과 같이 할 수 있어요:
func TestSomething(t *testing.T) {
// 테스트 셋업
db := setupDB(t)
defer db.Close() // 테스트 종료 후 정리
// 테스트 코드...
}
defer
를 사용하면 함수가 종료될 때 자동으로 정리 작업이 실행돼요. 이렇게 하면 테스트 코드가 훨씬 깔끔해지죠! 😊
5. 모킹과 의존성 관리 🎭
실제 애플리케이션에서는 데이터베이스, 외부 API, 파일 시스템 등 다양한 외부 의존성이 있어요. TDD를 효과적으로 적용하려면 이러한 의존성을 모킹(mocking)하는 방법을 알아야 해요.
5.1 인터페이스를 활용한 모킹
Go에서는 인터페이스를 사용해 의존성을 추상화하고, 테스트용 구현체를 만들 수 있어요. 앞서 본 UserRepository
인터페이스가 좋은 예시죠.
type UserRepository interface {
FindByID(id int) (User, error)
Save(user User) error
}
// 실제 구현체
type SQLUserRepository struct {
db *sql.DB
}
func (r *SQLUserRepository) FindByID(id int) (User, error) {
// 실제 데이터베이스 쿼리 실행
// ...
}
func (r *SQLUserRepository) Save(user User) error {
// 실제 데이터베이스에 저장
// ...
}
// 테스트용 모의 구현체
type MockUserRepository struct {
users map[int]User
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[int]User),
}
}
func (r *MockUserRepository) FindByID(id int) (User, error) {
user, exists := r.users[id]
if !exists {
return User{}, errors.New("사용자를 찾을 수 없음")
}
return user, nil
}
func (r *MockUserRepository) Save(user User) error {
r.users[user.ID] = user
return nil
}
이제 테스트에서는 실제 데이터베이스 대신 MockUserRepository
를 사용할 수 있어요:
func TestUserService_SaveUser(t *testing.T) {
// 모의 레포지토리 생성
repo := NewMockUserRepository()
// 테스트할 서비스 생성
service := NewUserService(repo)
// 테스트 데이터
user := User{ID: 1, Name: "Gopher"}
// 함수 실행
err := service.SaveUser(user)
// 결과 확인
if err != nil {
t.Errorf("에러가 발생하지 않아야 함: %v", err)
}
// 저장된 사용자 확인
savedUser, err := repo.FindByID(1)
if err != nil {
t.Errorf("사용자를 찾을 수 없음: %v", err)
}
if savedUser.Name != "Gopher" {
t.Errorf("사용자 이름이 'Gopher'여야 함, 실제: %s", savedUser.Name)
}
}
이런 방식으로 실제 데이터베이스 없이도 비즈니스 로직을 테스트할 수 있어요. 테스트가 빠르고 안정적으로 실행되는 장점이 있죠! 💪
5.2 모킹 라이브러리 활용하기
직접 모의 객체를 구현하는 것 외에도, 모킹 라이브러리를 사용할 수도 있어요. Go에서 인기 있는 모킹 라이브러리로는 gomock
과 testify/mock
이 있어요.
예를 들어, testify/mock
을 사용한 예시를 살펴볼게요:
import (
"testing"
"github.com/stretchr/testify/mock"
)
// 모의 레포지토리
type MockUserRepo struct {
mock.Mock
}
func (m *MockUserRepo) FindByID(id int) (User, error) {
args := m.Called(id)
return args.Get(0).(User), args.Error(1)
}
func (m *MockUserRepo) Save(user User) error {
args := m.Called(user)
return args.Error(0)
}
func TestUserService_GetUser_WithMockLibrary(t *testing.T) {
// 모의 레포지토리 생성
mockRepo := new(MockUserRepo)
// 모의 동작 설정
mockRepo.On("FindByID", 1).Return(User{ID: 1, Name: "Gopher"}, nil)
// 테스트할 서비스 생성
service := NewUserService(mockRepo)
// 함수 실행
user, err := service.GetUser(1)
// 결과 확인
if err != nil {
t.Errorf("에러가 발생하지 않아야 함: %v", err)
}
if user.Name != "Gopher" {
t.Errorf("사용자 이름이 'Gopher'여야 함, 실제: %s", user.Name)
}
// 예상대로 호출되었는지 확인
mockRepo.AssertExpectations(t)
}
모킹 라이브러리를 사용하면 모의 객체의 동작을 더 세밀하게 제어할 수 있어요. 특히 복잡한 상호작용을 테스트할 때 유용하답니다! 🔍
💡 꿀팁: 모킹은 유용하지만, 너무 많이 사용하면 테스트가 실제 동작과 달라질 수 있어요. 가능하면 통합 테스트도 함께 작성하여 실제 환경에서의 동작을 검증하는 것이 좋아요. 재능넷의 많은 Go 개발자들도 단위 테스트와 통합 테스트를 적절히 조합해서 사용한다고 해요!
6. 실제 프로젝트에 TDD 적용하기 💼
이론은 충분히 배웠으니, 이제 실제 프로젝트에 TDD를 적용하는 방법을 알아볼게요. 간단한 REST API를 만드는 예제를 통해 살펴볼게요.
6.1 프로젝트 구조 설계
먼저 프로젝트 구조를 설계해볼게요. 클린 아키텍처 원칙을 따라 레이어를 분리하면 테스트하기 좋은 구조가 됩니다.
project/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── domain/
│ │ └── user.go
│ ├── repository/
│ │ ├── user_repository.go
│ │ └── user_repository_test.go
│ ├── service/
│ │ ├── user_service.go
│ │ └── user_service_test.go
│ └── handler/
│ ├── user_handler.go
│ └── user_handler_test.go
└── pkg/
└── validator/
├── validator.go
└── validator_test.go
이런 구조로 각 레이어를 분리하면 의존성을 관리하기 쉽고, 각 컴포넌트를 독립적으로 테스트할 수 있어요. 진짜 꿀팁이에요! 🍯
6.2 도메인 모델 정의 및 테스트
TDD로 개발을 시작할 때는 도메인 모델부터 정의하고 테스트하는 것이 좋아요.
// internal/domain/user.go
package domain
import (
"errors"
"time"
)
type User struct {
ID int `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func NewUser(email, name string) (User, error) {
if email == "" {
return User{}, errors.New("이메일은 필수 입력 항목입니다")
}
if name == "" {
return User{}, errors.New("이름은 필수 입력 항목입니다")
}
now := time.Now()
return User{
Email: email,
Name: name,
CreatedAt: now,
UpdatedAt: now,
}, nil
}
func (u *User) Update(name string) error {
if name == "" {
return errors.New("이름은 필수 입력 항목입니다")
}
u.Name = name
u.UpdatedAt = time.Now()
return nil
}
이제 이 도메인 모델을 테스트해볼게요:
// internal/domain/user_test.go
package domain
import (
"testing"
"time"
)
func TestNewUser(t *testing.T) {
tests := []struct {
name string
email string
username string
wantErr bool
}{
{"유효한 사용자", "gopher@example.com", "Gopher", false},
{"이메일 누락", "", "Gopher", true},
{"이름 누락", "gopher@example.com", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
user, err := NewUser(tt.email, tt.username)
if tt.wantErr {
if err == nil {
t.Error("에러가 발생해야 하는데 발생하지 않음")
}
return
}
if err != nil {
t.Errorf("예상치 못한 에러 발생: %v", err)
}
if user.Email != tt.email {
t.Errorf("이메일이 %s여야 하는데 %s임", tt.email, user.Email)
}
if user.Name != tt.username {
t.Errorf("이름이 %s여야 하는데 %s임", tt.username, user.Name)
}
if time.Since(user.CreatedAt) > time.Second {
t.Error("CreatedAt이 현재 시간과 가까워야 함")
}
})
}
}
func TestUser_Update(t *testing.T) {
user, _ := NewUser("gopher@example.com", "Gopher")
originalUpdatedAt := user.UpdatedAt
// 잠시 대기하여 시간 차이가 생기도록 함
time.Sleep(10 * time.Millisecond)
err := user.Update("New Gopher")
if err != nil {
t.Errorf("예상치 못한 에러 발생: %v", err)
}
if user.Name != "New Gopher" {
t.Errorf("이름이 'New Gopher'여야 하는데 %s임", user.Name)
}
if !user.UpdatedAt.After(originalUpdatedAt) {
t.Error("UpdatedAt이 업데이트되어야 함")
}
// 빈 이름으로 업데이트 시도
err = user.Update("")
if err == nil {
t.Error("빈 이름으로 업데이트할 때 에러가 발생해야 함")
}
}
이런 식으로 도메인 모델의 모든 기능을 테스트하면서 개발할 수 있어요. 진짜 이렇게 하면 나중에 버그 잡느라 머리 쥐어뜯는 일이 확실히 줄어든답니다! 😌
6.3 레포지토리 레이어 테스트
다음으로 데이터 접근 레이어인 레포지토리를 테스트해볼게요. 여기서는 인터페이스를 정의하고, 구현체를 테스트합니다.
// internal/repository/user_repository.go
package repository
import (
"database/sql"
"errors"
"github.com/yourusername/project/internal/domain"
)
type UserRepository interface {
FindByID(id int) (domain.User, error)
FindByEmail(email string) (domain.User, error)
Save(user domain.User) (domain.User, error)
Update(user domain.User) error
Delete(id int) error
}
type SQLUserRepository struct {
db *sql.DB
}
func NewSQLUserRepository(db *sql.DB) *SQLUserRepository {
return &SQLUserRepository{db: db}
}
func (r *SQLUserRepository) FindByID(id int) (domain.User, error) {
// 실제 구현...
}
// 다른 메서드 구현...
레포지토리 테스트는 실제 데이터베이스를 사용하는 통합 테스트로 작성할 수도 있고, 모의 데이터베이스를 사용해 단위 테스트로 작성할 수도 있어요. 여기서는 통합 테스트 예시를 보여드릴게요:
// internal/repository/user_repository_test.go
package repository
import (
"database/sql"
"testing"
"github.com/yourusername/project/internal/domain"
_ "github.com/go-sql-driver/mysql"
)
func setupTestDB(t *testing.T) *sql.DB {
db, err := sql.Open("mysql", "user:password@/testdb")
if err != nil {
t.Fatalf("데이터베이스 연결 실패: %v", err)
}
// 테스트용 테이블 초기화
_, err = db.Exec("TRUNCATE TABLE users")
if err != nil {
t.Fatalf("테이블 초기화 실패: %v", err)
}
return db
}
func TestSQLUserRepository_Save(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
repo := NewSQLUserRepository(db)
user, _ := domain.NewUser("test@example.com", "Test User")
savedUser, err := repo.Save(user)
if err != nil {
t.Errorf("사용자 저장 중 에러 발생: %v", err)
}
if savedUser.ID == 0 {
t.Error("저장된 사용자에게 ID가 할당되어야 함")
}
// 저장된 사용자 조회
foundUser, err := repo.FindByID(savedUser.ID)
if err != nil {
t.Errorf("사용자 조회 중 에러 발생: %v", err)
}
if foundUser.Email != user.Email {
t.Errorf("이메일이 %s여야 하는데 %s임", user.Email, foundUser.Email)
}
}
// 다른 테스트 메서드...
실제 프로젝트에서는 테스트용 데이터베이스를 Docker 컨테이너로 실행하거나, SQLite 인메모리 데이터베이스를 사용하는 방법도 많이 사용해요. 이렇게 하면 테스트 환경을 더 쉽게 관리할 수 있답니다! 🐳
6.4 서비스 레이어 테스트
서비스 레이어는 비즈니스 로직을 담당하는 부분이에요. 여기서는 레포지토리를 모킹하여 테스트할 수 있어요.
// internal/service/user_service.go
package service
import (
"errors"
"github.com/yourusername/project/internal/domain"
"github.com/yourusername/project/internal/repository"
)
type UserService struct {
repo repository.UserRepository
}
func NewUserService(repo repository.UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) GetUserByID(id int) (domain.User, error) {
if id <= 0 {
return domain.User{}, errors.New("유효하지 않은 사용자 ID")
}
return s.repo.FindByID(id)
}
func (s *UserService) CreateUser(email, name string) (domain.User, error) {
// 이메일 중복 확인
_, err := s.repo.FindByEmail(email)
if err == nil {
return domain.User{}, errors.New("이미 사용 중인 이메일입니다")
}
// 새 사용자 생성
user, err := domain.NewUser(email, name)
if err != nil {
return domain.User{}, err
}
// 저장
return s.repo.Save(user)
}
// 다른 메서드...
이제 서비스 레이어를 테스트해볼게요:
// internal/service/user_service_test.go
package service
import (
"errors"
"testing"
"github.com/yourusername/project/internal/domain"
)
// 모의 레포지토리
type MockUserRepository struct {
users map[int]domain.User
emails map[string]domain.User
nextID int
}
func NewMockUserRepository() *MockUserRepository {
return &MockUserRepository{
users: make(map[int]domain.User),
emails: make(map[string]domain.User),
nextID: 1,
}
}
func (r *MockUserRepository) FindByID(id int) (domain.User, error) {
user, exists := r.users[id]
if !exists {
return domain.User{}, errors.New("사용자를 찾을 수 없음")
}
return user, nil
}
func (r *MockUserRepository) FindByEmail(email string) (domain.User, error) {
user, exists := r.emails[email]
if !exists {
return domain.User{}, errors.New("사용자를 찾을 수 없음")
}
return user, nil
}
func (r *MockUserRepository) Save(user domain.User) (domain.User, error) {
user.ID = r.nextID
r.nextID++
r.users[user.ID] = user
r.emails[user.Email] = user
return user, nil
}
// 다른 메서드 구현...
func TestUserService_CreateUser(t *testing.T) {
repo := NewMockUserRepository()
service := NewUserService(repo)
// 성공 케이스
user, err := service.CreateUser("new@example.com", "New User")
if err != nil {
t.Errorf("사용자 생성 중 에러 발생: %v", err)
}
if user.ID == 0 {
t.Error("생성된 사용자에게 ID가 할당되어야 함")
}
// 중복 이메일 케이스
_, err = service.CreateUser("new@example.com", "Another User")
if err == nil {
t.Error("중복된 이메일로 사용자를 생성할 때 에러가 발생해야 함")
}
// 유효하지 않은 입력 케이스
_, err = service.CreateUser("", "Invalid User")
if err == nil {
t.Error("빈 이메일로 사용자를 생성할 때 에러가 발생해야 함")
}
}
// 다른 테스트 메서드...
이렇게 모의 레포지토리를 사용하면 데이터베이스 없이도 비즈니스 로직을 철저하게 테스트할 수 있어요. 진짜 개발 속도가 빨라지는 느낌! 🏃♂️
6.5 HTTP 핸들러 테스트
마지막으로 HTTP 핸들러 레이어를 테스트해볼게요. Go의 httptest
패키지를 사용하면 HTTP 요청을 모의로 만들어 테스트할 수 있어요.
// internal/handler/user_handler.go
package handler
import (
"encoding/json"
"net/http"
"strconv"
"github.com/gorilla/mux"
"github.com/yourusername/project/internal/service"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, err := strconv.Atoi(vars["id"])
if err != nil {
http.Error(w, "유효하지 않은 사용자 ID", http.StatusBadRequest)
return
}
user, err := h.userService.GetUserByID(id)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
type CreateUserRequest struct {
Email string `json:"email"`
Name string `json:"name"`
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "잘못된 요청 형식", http.StatusBadRequest)
return
}
user, err := h.userService.CreateUser(req.Email, req.Name)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
// 다른 핸들러 메서드...
이제 핸들러를 테스트해볼게요:
// internal/handler/user_handler_test.go
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/mux"
"github.com/yourusername/project/internal/domain"
"github.com/yourusername/project/internal/service"
)
// 모의 서비스
type MockUserService struct {
users map[int]domain.User
}
func NewMockUserService() *MockUserService {
return &MockUserService{
users: map[int]domain.User{
1: {ID: 1, Email: "test@example.com", Name: "Test User"},
},
}
}
func (s *MockUserService) GetUserByID(id int) (domain.User, error) {
user, exists := s.users[id]
if !exists {
return domain.User{}, http.ErrNotFound
}
return user, nil
}
func (s *MockUserService) CreateUser(email, name string) (domain.User, error) {
// 간단한 구현
newID := len(s.users) + 1
user, _ := domain.NewUser(email, name)
user.ID = newID
s.users[newID] = user
return user, nil
}
// 다른 메서드 구현...
func TestUserHandler_GetUser(t *testing.T) {
mockService := NewMockUserService()
handler := NewUserHandler(mockService)
// 라우터 설정
r := mux.NewRouter()
r.HandleFunc("/users/{id}", handler.GetUser).Methods("GET")
// 테스트 서버 생성
server := httptest.NewServer(r)
defer server.Close()
// 요청 실행
resp, err := http.Get(server.URL + "/users/1")
if err != nil {
t.Fatalf("요청 실패: %v", err)
}
defer resp.Body.Close()
// 상태 코드 확인
if resp.StatusCode != http.StatusOK {
t.Errorf("상태 코드가 200이어야 하는데 %d임", resp.StatusCode)
}
// 응답 본문 확인
var user domain.User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
t.Fatalf("응답 디코딩 실패: %v", err)
}
if user.ID != 1 {
t.Errorf("사용자 ID가 1이어야 하는데 %d임", user.ID)
}
if user.Email != "test@example.com" {
t.Errorf("이메일이 test@example.com이어야 하는데 %s임", user.Email)
}
}
func TestUserHandler_CreateUser(t *testing.T) {
mockService := NewMockUserService()
handler := NewUserHandler(mockService)
// 라우터 설정
r := mux.NewRouter()
r.HandleFunc("/users", handler.CreateUser).Methods("POST")
// 요청 본문 생성
reqBody := CreateUserRequest{
Email: "new@example.com",
Name: "New User",
}
body, _ := json.Marshal(reqBody)
// 요청 생성
req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
// 응답 레코더 생성
rr := httptest.NewRecorder()
// 핸들러 실행
r.ServeHTTP(rr, req)
// 상태 코드 확인
if rr.Code != http.StatusCreated {
t.Errorf("상태 코드가 201이어야 하는데 %d임", rr.Code)
}
// 응답 본문 확인
var user domain.User
if err := json.NewDecoder(rr.Body).Decode(&user); err != nil {
t.Fatalf("응답 디코딩 실패: %v", err)
}
if user.Email != "new@example.com" {
t.Errorf("이메일이 new@example.com이어야 하는데 %s임", user.Email)
}
}
이렇게 각 레이어별로 테스트를 작성하면 전체 애플리케이션이 제대로 동작하는지 확인할 수 있어요. 특히 HTTP 핸들러 테스트는 API가 의도한 대로 동작하는지 검증하는 데 매우 중요하답니다! 🌐
💡 실무 팁: 실제 프로젝트에서는 CI/CD 파이프라인에 테스트를 통합하는 것이 좋아요. 코드가 저장소에 푸시될 때마다 자동으로 테스트가 실행되도록 설정하면, 문제를 조기에 발견할 수 있어요. 재능넷에서 활동하는 많은 Go 개발자들도 GitHub Actions나 GitLab CI를 활용해 자동 테스트를 구축한다고 해요!
7. TDD의 어려움과 극복 방법 🧗♂️
- 지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
재능넷은 최신 AI 기술과 법률에 기반하여 자사의 지적 재산권을 적극적으로 보호하며,
무단 사용 및 침해 행위에 대해 법적 대응을 할 권리를 보유합니다.
© 2025 재능넷 | All rights reserved.
댓글 0개