고 중급 #6 테스팅 — testing 패키지와 table-driven

6 분 소요

#5 context 깊이 다음, 이번엔 — Go의 또 다른 자랑 중 하나인 표준 라이브러리에 들어 있는 강력한 테스팅 도구를 정리합니다.

첫 테스트 #

Go의 테스팅은 — 표준 testing 패키지와 go test 명령으로 동작합니다. 추가 라이브러리 없이.

add.go
package math

func Add(a, b int) int {
	return a + b
}
add_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)
	}
}

규칙:

  1. 파일 이름이 _test.go로 끝남
  2. 함수 이름이 Test로 시작, 매개변수는 *testing.T
  3. 같은 패키지 안에 둘 수 있음 (또는 package math_test로 외부 패키지)

실행 #

테스트 실행
go test                  # 현재 패키지
go test ./...            # 모든 패키지
go test -v               # 자세히 (각 테스트 결과)
go test -run TestAdd     # 특정 테스트만

go test -v가 자주 쓰여요 — 어떤 테스트가 실행되고 어떤 게 실패했는지 명확히 보입니다.

실패 알리기 — t.Errorf vs t.Fatalf #

Error vs Fatal
func TestSomething(t *testing.T) {
	if !condition1 {
		t.Errorf("조건 1 실패")    // 실패 기록 + 다음 줄도 실행
	}
	if !condition2 {
		t.Fatalf("조건 2 실패")    // 실패 기록 + 즉시 종료
	}
	// ...
}
Errorf / ErrorFatalf / Fatal
실패 기록
그 뒤 코드 실행✗ (즉시 종료)
어울리는 경우독립 검증 (여러 개를 한 테스트에)이후 코드가 의미 없는 상황

보통 Fatal은 — 셋업이 실패했거나 nil 포인터 등 더 가도 의미 없는 경우에 씁니다.

Table-Driven 테스트 #

같은 함수에 여러 입력을 검사하고 싶을 때 — Go의 가장 유명한 테스트 패턴입니다.

table-driven
func TestAdd(t *testing.T) {
	tests := []struct {
		name string
		a, b int
		want int
	}{
		{"양수", 2, 3, 5},
		{"음수", -1, -2, -3},
		{"섞임", -5, 10, 5},
		{"영", 0, 0, 0},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := Add(tc.a, tc.b)
			if got != tc.want {
				t.Errorf("Add(%d, %d) = %d, want %d", tc.a, tc.b, got, tc.want)
			}
		})
	}
}

핵심:

  1. 케이스를 슬라이스로 정의 — 새 케이스 추가가 한 줄
  2. t.Run(name, ...)으로 서브테스트 — 각 케이스가 독립적으로 실행되고 보고됨
  3. 케이스마다 name이 있어 어느 게 실패했는지 즉시 알 수 있음

go test -v 결과:

=== RUN   TestAdd
=== RUN   TestAdd/양수
=== RUN   TestAdd/음수
=== RUN   TestAdd/섞임
=== RUN   TestAdd/영
--- PASS: TestAdd (0.00s)
    --- PASS: TestAdd/양수 (0.00s)
    --- PASS: TestAdd/음수 (0.00s)
    --- PASS: TestAdd/섞임 (0.00s)
    --- PASS: TestAdd/영 (0.00s)

특정 서브테스트만 실행 #

서브테스트 필터
go test -run "TestAdd/양수"

이게 거의 모든 Go 코드의 표준 테스트 모양입니다. 케이스를 데이터로 표현하고, 테스트 로직은 한 곳에.

헬퍼 함수 — t.Helper() #

테스트에서 자주 쓰는 검사를 함수로 빼면, 실패 메시지에 헬퍼 함수 위치가 표시되어 어디서 실패했는지 보기 어렵습니다.

t.Helper()
func assertEqual(t *testing.T, got, want int) {
	t.Helper()    // 이 함수의 호출자 위치를 보고
	if got != want {
		t.Errorf("got %d, want %d", got, want)
	}
}

func TestAdd(t *testing.T) {
	assertEqual(t, Add(2, 3), 5)
}

t.Helper() 한 줄을 추가하면 — 실패 메시지에 헬퍼 함수가 아니라 호출 위치가 표시됩니다. 헬퍼를 자주 만든다면 거의 필수.

셋업과 정리 — t.Cleanup #

테스트마다 셋업/정리가 필요하면.

setup / cleanup
func TestWithDB(t *testing.T) {
	db := setupDB(t)
	t.Cleanup(func() {
		db.Close()
	})

	// 테스트 본문
}

t.Cleanup테스트가 끝날 때 자동 실행됩니다. 성공/실패 상관없이. defer와 비슷하지만 — 테스트 헬퍼 함수에서도 동작합니다(헬퍼가 끝나도 cleanup은 테스트 끝까지 살아 있음).

임시 디렉토리 — t.TempDir #

t.TempDir
func TestFileWrite(t *testing.T) {
	dir := t.TempDir()    // 자동으로 정리되는 임시 디렉토리

	path := filepath.Join(dir, "test.txt")
	os.WriteFile(path, []byte("hi"), 0644)

	// 테스트 끝나면 dir 자동 삭제
}

자주 쓰이는 헬퍼. 직접 os.MkdirTempdefer os.RemoveAll을 짜는 것보다 짧고 안전합니다.

외부 패키지 테스트 — _test 접미사 #

같은 폴더의 테스트 파일은 보통 같은 패키지 이름 — 그러면 unexported 함수도 테스트 가능. 다만 의도적으로 외부에서 보는 시점으로 테스트하고 싶을 때:

외부 패키지 테스트
// math/add_test.go
package math_test    // _test 접미사

import (
	"testing"
	"path/to/math"   // 외부에서 import 하듯
)

func TestAdd(t *testing.T) {
	got := math.Add(2, 3)
	// ...
}

이러면 — exported API만으로 테스트하게 강제됩니다. 큰 라이브러리에서는 두 종류를 섞어 쓰기도 합니다.

벤치마크 — BenchmarkXxx #

벤치마크
func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		Add(2, 3)
	}
}

규칙:

  1. 함수 이름 Benchmark로 시작, 매개변수 *testing.B
  2. for i := 0; i < b.N; i++ 안에 측정할 코드
  3. b.N은 Go가 자동으로 조정 — 반복 횟수가 의미 있는 측정에 충분해질 때까지
벤치마크 실행
go test -bench=.
go test -bench=. -benchmem    # 메모리 할당도 측정
결과 예시
BenchmarkAdd-8          1000000000      0.30 ns/op

Add 한 번이 평균 0.30 나노초 걸렸다는 뜻. CPU 코어 수(8)도 함께 표시.

b.ResetTimer() — 셋업 비용 제외 #

ResetTimer
func BenchmarkParse(b *testing.B) {
	data := generateBigData()    // 셋업 — 측정에서 제외
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		Parse(data)
	}
}

셋업 시간이 측정에 들어가면 정확한 결과가 안 나오니 — 셋업 후 ResetTimer().

Example 함수 — 문서이자 테스트 #

example
func ExampleAdd() {
	fmt.Println(Add(2, 3))
	// Output: 5
}

Example로 시작하는 함수 + 끝에 // Output: 주석. Go의 godoc/pkg.go.dev에 코드 예제로 표시되고, 동시에 테스트로도 실행됩니다. Output이 다르면 실패.

라이브러리 문서를 풍부하게 만드는 표준 도구입니다.

모킹 — Go의 접근 방식 #

Go에는 mocking 라이브러리(testify, gomock)도 있지만 — 인터페이스만 잘 쓰면 표준 도구만으로 모킹이 됩니다.

인터페이스로 모킹
type UserStore interface {
	GetUser(id string) (*User, error)
}

type Service struct {
	store UserStore
}

// 실제 구현
type DBStore struct{ /* ... */ }
func (s *DBStore) GetUser(id string) (*User, error) { /* DB 조회 */ }

// 테스트용 fake
type fakeStore struct {
	users map[string]*User
}
func (s *fakeStore) GetUser(id string) (*User, error) {
	if u, ok := s.users[id]; ok {
		return u, nil
	}
	return nil, ErrNotFound
}

// 테스트
func TestService(t *testing.T) {
	store := &fakeStore{
		users: map[string]*User{"u1": {Name: "커티스"}},
	}
	svc := &Service{store: store}

	user, err := svc.LookupUser("u1")
	// ...
}

#1 인터페이스의 암묵적 구현 덕분에 — 테스트용 구현체를 자유롭게 끼워넣을 수 있습니다. mocking 라이브러리 없이도 충분합니다.

httptest — HTTP 테스트 #

표준 라이브러리에 HTTP 핸들러를 테스트하는 도구가 있습니다.

httptest.NewServer
import (
	"net/http"
	"net/http/httptest"
)

func TestHandler(t *testing.T) {
	handler := http.HandlerFunc(myHandler)
	server := httptest.NewServer(handler)
	defer server.Close()

	resp, err := http.Get(server.URL + "/api/users")
	// 검증
}

httptest.NewRecorder도 — 진짜 서버를 띄우지 않고 핸들러만 호출해서 결과를 검사할 수 있습니다. 실전 #6 테스트와 배포에서 자세히.

커버리지 — -cover #

커버리지
go test -cover
go test -coverprofile=cover.out
go tool cover -html=cover.out      # HTML로 시각화

테스트가 어떤 줄을 실행했는지 — 한 명령으로. 의외로 가벼운 도구입니다.

자주 쓰는 테스트 라이브러리 — testify #

표준만으로 충분하지만 — assertion이 길어지면 testify가 자주 쓰입니다.

testify
import "github.com/stretchr/testify/assert"

func TestSomething(t *testing.T) {
	got := Add(2, 3)
	assert.Equal(t, 5, got)
	assert.NoError(t, err)
	assert.NotNil(t, result)
}

if/Errorf보다 짧고 의도가 분명. 다만 표준만으로도 충분히 동작하니, 팀 컨벤션에 맞추면 됩니다.

마무리 #

이번 글에서 정리한 내용:

  • 파일 _test.go, 함수 Test..., 매개변수 *testing.T
  • t.Errorf (계속) vs t.Fatalf (즉시 중단)
  • table-driven + t.Run이 Go의 표준 패턴
  • t.Helper()로 헬퍼 함수에서 호출자 위치 표시
  • t.Cleanup, t.TempDir 같은 자동 정리 도구
  • _test 접미사 패키지로 외부 시점 테스트
  • Benchmark... + b.N + -bench=.
  • Example...은 문서이자 테스트
  • 인터페이스 + fake로 표준 모킹
  • httptest, -cover 같은 빌트인 지원

다음 글(#7 표준 라이브러리 투어)에서는 — Go의 두꺼운 표준 라이브러리 중 자주 쓰이는 것들 (io, fmt, strings, time, encoding/json 등)을 한 번에 훑습니다.

X