Go 기본 테스트 — testing.T와 테이블 주도 테스트
Go는 표준 라이브러리에 testing 패키지가 내장되어 있습니다. 별도 프레임워크 없이도 완전한 테스트 환경을 구성할 수 있습니다.
Go 테스트의 특징
- 테스트 파일은
_test.go접미사를 사용 - 테스트 함수는
Test로 시작,*testing.T매개변수 go test ./...명령으로 실행- 빌드 시 테스트 파일은 제외됨
기본 테스트 작성
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("0으로 나눌 수 없습니다")
}
return a / b, nil
}
func IsPrime(n int) bool {
if n < 2 {
return false
}
for i := 2; i*i <= n; i++ {
if n%i == 0 {
return false
}
}
return true
}
// math_test.go
package math
import (
"testing"
)
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
// t.Errorf: 실패 기록 후 계속 실행
t.Errorf("Add(2, 3) = %d; 원하는 값 %d", result, expected)
}
}
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("예상치 못한 에러: %v", err) // 즉시 종료
}
if result != 5.0 {
t.Errorf("Divide(10, 2) = %f; 원하는 값 %f", result, 5.0)
}
}
func TestDivideByZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("0으로 나눌 때 에러가 발생해야 함")
}
}
테스트 실행 명령어
# 현재 패키지 테스트
go test
# 전체 패키지 테스트
go test ./...
# 상세 출력 (-v)
go test -v ./...
# 특정 테스트만 실행
go test -run TestAdd ./...
# 패턴 매칭
go test -run TestDivide ./...
# 커버리지 측정
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # 브라우저로 확인
테이블 주도 테스트 (Table-Driven Tests)
Go의 표준 테스트 패턴입니다. 여러 입력/출력 쌍을 구조체 슬라이스로 정의합니다.
func TestAddTableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"양수 더하기", 2, 3, 5},
{"음수 더하기", -1, -2, -3},
{"양수와 음수", 5, -3, 2},
{"0과 더하기", 0, 5, 5},
{"큰 수", 1000000, 999999, 1999999},
}
for _, tt := range tests {
// t.Run으로 각 케이스를 서브테스트로 실행
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; 원하는 값 %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestIsPrimeTableDriven(t *testing.T) {
tests := []struct {
n int
expected bool
}{
{1, false},
{2, true},
{3, true},
{4, false},
{17, true},
{100, false},
{97, true},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("IsPrime(%d)", tt.n), func(t *testing.T) {
result := IsPrime(tt.n)
if result != tt.expected {
t.Errorf("IsPrime(%d) = %v; 원하는 값 %v", tt.n, result, tt.expected)
}
})
}
}
에러 케이스 포함 테이블 테스트
func TestDivideTable(t *testing.T) {
tests := []struct {
name string
a, b float64
expected float64
wantError bool
}{
{"정상 나눗셈", 10.0, 2.0, 5.0, false},
{"소수", 7.0, 2.0, 3.5, false},
{"0으로 나누기", 10.0, 0.0, 0.0, true},
{"음수 나눗셈", -10.0, 2.0, -5.0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.wantError {
if err == nil {
t.Error("에러가 발생해야 하지만 nil 반환됨")
}
return
}
if err != nil {
t.Fatalf("예상치 못한 에러: %v", err)
}
if result != tt.expected {
t.Errorf("Divide(%g, %g) = %g; 원하는 값 %g",
tt.a, tt.b, result, tt.expected)
}
})
}
}
헬퍼 함수 패턴
// t.Helper() 호출로 실패 줄 번호가 헬퍼가 아닌 호출자 위치를 가리킴
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("예상치 못한 에러: %v", err)
}
}
func assertError(t *testing.T, err error) {
t.Helper()
if err == nil {
t.Fatal("에러가 발생해야 함")
}
}
// 사용 예
func TestWithHelpers(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5)
_, err := Divide(10, 0)
assertError(t, err)
}
testify 라이브러리 — 더 편리한 단언(Assert)
go get github.com/stretchr/testify/assert
go get github.com/stretchr/testify/require
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWithTestify(t *testing.T) {
// assert: 실패해도 계속 실행
result := Add(2, 3)
assert.Equal(t, 5, result, "Add(2, 3)은 5여야 함")
assert.NotEqual(t, 0, result)
// require: 실패 시 즉시 종료 (테스트 의존성 있을 때 사용)
user, err := findUser(1)
require.NoError(t, err, "사용자 조회는 에러 없어야 함")
require.NotNil(t, user)
assert.Equal(t, "김고랭", user.Name)
assert.Greater(t, user.Age, 0)
// 슬라이스/맵 비교
assert.ElementsMatch(t, []int{3, 1, 2}, []int{1, 2, 3})
assert.Contains(t, "hello world", "world")
}
TestMain — 테스트 생명주기 제어
func TestMain(m *testing.M) {
// 모든 테스트 실행 전 설정
fmt.Println("테스트 환경 설정 중...")
setupTestEnvironment()
// 테스트 실행
code := m.Run()
// 테스트 실행 후 정리
fmt.Println("테스트 환경 정리 중...")
teardownTestEnvironment()
os.Exit(code)
}
병렬 테스트
func TestParallel(t *testing.T) {
tests := []struct {
name string
input int
}{
{"케이스1", 1},
{"케이스2", 2},
{"케이스3", 3},
}
for _, tt := range tests {
tt := tt // 루프 변수 캡처 (Go 1.22 이전 필요)
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 서브테스트를 병렬로 실행
// 독립적인 테스트 작업
result := expensiveOperation(tt.input)
assert.Greater(t, result, 0)
})
}
}
Setup & Teardown 패턴
type TestSuite struct {
db *sql.DB
repo *UserRepository
}
func setupSuite(t *testing.T) (*TestSuite, func()) {
t.Helper()
db := setupTestDB(t)
suite := &TestSuite{
db: db,
repo: NewUserRepository(db),
}
cleanup := func() {
db.Exec("TRUNCATE TABLE users CASCADE")
}
return suite, cleanup
}
func TestUserRepository(t *testing.T) {
suite, cleanup := setupSuite(t)
defer cleanup()
t.Run("사용자 생성", func(t *testing.T) {
user, err := suite.repo.Create(context.Background(), "테스트", "test@ex.com", 25)
require.NoError(t, err)
assert.NotZero(t, user.ID)
})
t.Run("사용자 조회", func(t *testing.T) {
// ...
})
}
커버리지 분석
# 커버리지 측정
go test -coverprofile=coverage.out ./...
# 함수별 커버리지 확인
go tool cover -func=coverage.out
# HTML 리포트 (브라우저)
go tool cover -html=coverage.out -o coverage.html
# 특정 패키지만
go test -cover -covermode=atomic ./internal/...
핵심 정리
| 함수/메서드 | 용도 |
|---|---|
t.Error(f) | 실패 기록 후 계속 실행 |
t.Fatal(f) | 실패 기록 후 즉시 종료 |
t.Log(f) | 로그 출력 (실패 시만 표시) |
t.Helper() | 헬퍼 함수 표시 |
t.Parallel() | 병렬 테스트 선언 |
t.Run(name, f) | 서브테스트 실행 |
t.Skip() | 테스트 건너뜀 |
- 테이블 주도 테스트로 코드 중복 최소화
t.Helper()는 헬퍼 함수에 항상 추가- 병렬 테스트에서 루프 변수 캡처 주의 (Go 1.22+ 자동 해결)