| name | golang-testing |
| description | 테이블 기반 테스트, 테스트 헬퍼, 벤치마킹, 레이스 컨디션 감지, 커버리지 분석 및 통합 테스트 패턴을 포함한 Go 테스트 모범 사례. Go 테스트를 작성하거나 개선할 때 사용하세요.
|
| metadata | {"origin":"ECC","globs":["**/*.go","**/go.mod","**/go.sum"]} |
Go 테스트 (Go Testing)
이 스킬은 일반적인 테스트 원칙을 Go 특유의 관용구로 확장한 포괄적인 Go 테스트 패턴을 제공합니다.
테스트 프레임워크
표준 go test와 함께 **테이블 기반 테스트(table-driven tests)**를 기본 패턴으로 사용하세요.
테이블 기반 테스트
Go의 관용적인 테스트 패턴:
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{
name: "유효한 이메일",
email: "user@example.com",
wantErr: false,
},
{
name: "@ 누락",
email: "userexample.com",
wantErr: true,
},
{
name: "빈 문자열",
email: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail(%q) 에러 = %v, wantErr %v",
tt.email, err, tt.wantErr)
}
})
}
}
이점:
- 새로운 테스트 케이스 추가가 용이함
- 명확한 테스트 케이스 문서화
t.Parallel()을 통한 병렬 테스트 실행
t.Run()을 통한 독립적인 서브 테스트
테스트 헬퍼 (Test Helpers)
헬퍼 함수를 표시하기 위해 t.Helper()를 사용하세요:
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("예기치 않은 에러: %v", err)
}
}
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("결과: %v, 기대값: %v", got, want)
}
}
이점:
- 테스트 실패 시 정확한 라인 번호 표시
- 재사용 가능한 테스트 유틸리티
- 깔끔한 테스트 코드
테스트 픽스처 (Test Fixtures)
리소스 정리를 위해 t.Cleanup()을 사용하세요:
func testDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("테스트 DB 오픈 실패: %v", err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("DB 닫기 실패: %v", err)
}
})
return db
}
func TestUserRepository(t *testing.T) {
db := testDB(t)
repo := NewUserRepository(db)
}
레이스 감지 (Race Detection)
데이터 레이스를 감지하려면 항상 -race 플래그와 함께 테스트를 실행하세요:
go test -race ./...
CI/CD 환경:
- name: 레이스 감지기와 함께 테스트 실행
run: go test -race -timeout 5m ./...
이유:
- 동시성 접근 버그 감지
- 프로덕션의 레이스 컨디션 방지
- 테스트 시 성능 오버헤드 최소화
커버리지 분석 (Coverage Analysis)
기본 커버리지
go test -cover ./...
상세 커버리지 보고서
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
커버리지 임계값
go test -cover ./... | grep -E 'coverage: [0-7][0-9]\.[0-9]%' && exit 1
벤치마킹 (Benchmarking)
func BenchmarkValidateEmail(b *testing.B) {
email := "user@example.com"
b.ResetTimer()
for i := 0; i < b.N; i++ {
ValidateEmail(email)
}
}
벤치마크 실행:
go test -bench=. -benchmem
벤치마크 비교:
go test -bench=. -benchmem > old.txt
go test -bench=. -benchmem > new.txt
benchstat old.txt new.txt
모킹 (Mocking)
인터페이스 기반 모킹
type UserRepository interface {
GetUser(id string) (*User, error)
}
type mockUserRepository struct {
users map[string]*User
err error
}
func (m *mockUserRepository) GetUser(id string) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func TestUserService(t *testing.T) {
mock := &mockUserRepository{
users: map[string]*User{
"1": {ID: "1", Name: "Alice"},
},
}
service := NewUserService(mock)
}
통합 테스트 (Integration Tests)
빌드 태그 (Build Tags)
package user_test
func TestUserRepository_Integration(t *testing.T) {
}
통합 테스트 실행:
go test -tags=integration ./...
테스트 컨테이너 (Test Containers)
func TestWithPostgres(t *testing.T) {
if testing.Short() {
t.Skip("통합 테스트 건너뜀")
}
ctx := context.Background()
container, err := testcontainers.GenericContainer(ctx, ...)
assertNoError(t, err)
t.Cleanup(func() {
container.Terminate(ctx)
})
}
테스트 조직 (Test Organization)
파일 구조
package/
├── user.go
├── user_test.go # 단위 테스트
├── user_integration_test.go # 통합 테스트
└── testdata/ # 테스트 피스처
└── users.json
패키지 명명 (Package Naming)
package user_test
package user
일반적인 패턴
HTTP 핸들러 테스트
func TestUserHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/users/1", nil)
rec := httptest.NewRecorder()
handler := NewUserHandler(mockRepo)
handler.ServeHTTP(rec, req)
assertEqual(t, rec.Code, http.StatusOK)
}
컨텍스트(Context)를 사용한 테스트
func TestWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
err := SlowOperation(ctx)
if !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("타임아웃 에러를 기대했지만 %v 결과가 나옴", err)
}
}
모범 사례
- 독립적인 테스트에는
t.Parallel() 사용
- 느린 테스트를 건너뛰려면
testing.Short() 사용
- 임시 디렉토리에는
t.TempDir() 사용
- 환경 변수 설정에는
t.Setenv() 사용
- 테스트 파일에서
init() 사용 지양
- 테스트 집중도 유지 - 테스트당 하나의 동작만 확인
- 의미 있는 테스트 이름 사용 - 테스트 대상을 명확히 설명
이 스킬을 사용하는 시점
- 새로운 Go 테스트를 작성할 때
- 테스트 커버리지를 개선할 때
- 테스트 인프라를 구축할 때
- 간헐적으로 실패하는(flaky) 테스트를 디버깅할 때
- 테스트 성능을 최적화할 때
- 통합 테스트를 구현할 때