고 중급 #6 테스팅 — testing 패키지와 table-driven
#5 context 깊이 다음, 이번엔 — Go의 또 다른 자랑 중 하나인 표준 라이브러리에 들어 있는 강력한 테스팅 도구를 정리합니다.
첫 테스트 #
Go의 테스팅은 — 표준 testing 패키지와 go test 명령으로 동작합니다. 추가 라이브러리 없이.
package math
func Add(a, b int) int {
return a + b
}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)
}
}규칙:
- 파일 이름이
_test.go로 끝남 - 함수 이름이
Test로 시작, 매개변수는*testing.T - 같은 패키지 안에 둘 수 있음 (또는
package math_test로 외부 패키지)
실행 #
go test # 현재 패키지
go test ./... # 모든 패키지
go test -v # 자세히 (각 테스트 결과)
go test -run TestAdd # 특정 테스트만go test -v가 자주 쓰여요 — 어떤 테스트가 실행되고 어떤 게 실패했는지 명확히 보입니다.
실패 알리기 — t.Errorf vs t.Fatalf
#
func TestSomething(t *testing.T) {
if !condition1 {
t.Errorf("조건 1 실패") // 실패 기록 + 다음 줄도 실행
}
if !condition2 {
t.Fatalf("조건 2 실패") // 실패 기록 + 즉시 종료
}
// ...
}Errorf / Error | Fatalf / Fatal | |
|---|---|---|
| 실패 기록 | ✓ | ✓ |
| 그 뒤 코드 실행 | ✓ | ✗ (즉시 종료) |
| 어울리는 경우 | 독립 검증 (여러 개를 한 테스트에) | 이후 코드가 의미 없는 상황 |
보통 Fatal은 — 셋업이 실패했거나 nil 포인터 등 더 가도 의미 없는 경우에 씁니다.
Table-Driven 테스트 #
같은 함수에 여러 입력을 검사하고 싶을 때 — Go의 가장 유명한 테스트 패턴입니다.
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)
}
})
}
}핵심:
- 케이스를 슬라이스로 정의 — 새 케이스 추가가 한 줄
t.Run(name, ...)으로 서브테스트 — 각 케이스가 독립적으로 실행되고 보고됨- 케이스마다
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()
#
테스트에서 자주 쓰는 검사를 함수로 빼면, 실패 메시지에 헬퍼 함수 위치가 표시되어 어디서 실패했는지 보기 어렵습니다.
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
#
테스트마다 셋업/정리가 필요하면.
func TestWithDB(t *testing.T) {
db := setupDB(t)
t.Cleanup(func() {
db.Close()
})
// 테스트 본문
}t.Cleanup은 테스트가 끝날 때 자동 실행됩니다. 성공/실패 상관없이. defer와 비슷하지만 — 테스트 헬퍼 함수에서도 동작합니다(헬퍼가 끝나도 cleanup은 테스트 끝까지 살아 있음).
임시 디렉토리 — t.TempDir
#
func TestFileWrite(t *testing.T) {
dir := t.TempDir() // 자동으로 정리되는 임시 디렉토리
path := filepath.Join(dir, "test.txt")
os.WriteFile(path, []byte("hi"), 0644)
// 테스트 끝나면 dir 자동 삭제
}자주 쓰이는 헬퍼. 직접 os.MkdirTemp와 defer 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)
}
}규칙:
- 함수 이름
Benchmark로 시작, 매개변수*testing.B for i := 0; i < b.N; i++안에 측정할 코드b.N은 Go가 자동으로 조정 — 반복 횟수가 의미 있는 측정에 충분해질 때까지
go test -bench=.
go test -bench=. -benchmem # 메모리 할당도 측정BenchmarkAdd-8 1000000000 0.30 ns/opAdd 한 번이 평균 0.30 나노초 걸렸다는 뜻. CPU 코어 수(8)도 함께 표시.
b.ResetTimer() — 셋업 비용 제외
#
func BenchmarkParse(b *testing.B) {
data := generateBigData() // 셋업 — 측정에서 제외
b.ResetTimer()
for i := 0; i < b.N; i++ {
Parse(data)
}
}셋업 시간이 측정에 들어가면 정확한 결과가 안 나오니 — 셋업 후 ResetTimer().
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 핸들러를 테스트하는 도구가 있습니다.
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가 자주 쓰입니다.
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(계속) vst.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 등)을 한 번에 훑습니다.