テスト駆動開発(TDD)とGoコードの高品質を保証するための包括的なテスト戦略。
失敗するテストを書き、実装し、リファクタリングするサイクルに従います。
// 1. テストを書く(失敗)
func TestCalculateTotal(t *testing.T) {
total := CalculateTotal([]float64{10.0, 20.0, 30.0})
want := 60.0
if total != want {
t.Errorf("got %f, want %f", total, want)
}
}
// 2. 実装する(テストを通す)
func CalculateTotal(prices []float64) float64 {
var total float64
for _, price := range prices {
total += price
}
return total
}
// 3. リファクタリング
// テストを壊さずにコードを改善
複数のケースを体系的にテストします。
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed signs", -2, 3, 1},
{"zeros", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.want {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, got, tt.want)
}
})
}
}
サブテストを使用した論理的なテストの構成。
func TestUser(t *testing.T) {
t.Run("validation", func(t *testing.T) {
t.Run("empty email", func(t *testing.T) {
user := User{Email: ""}
if err := user.Validate(); err == nil {
t.Error("expected validation error")
}
})
t.Run("valid email", func(t *testing.T) {
user := User{Email: "test@example.com"}
if err := user.Validate(); err != nil {
t.Errorf("unexpected error: %v", err)
}
})
})
t.Run("serialization", func(t *testing.T) {
// 別のテストグループ
})
}
mypackage/
├── user.go
├── user_test.go # ユニットテスト
├── integration_test.go # 統合テスト
├── testdata/ # テストフィクスチャ
│ ├── valid_user.json
│ └── invalid_user.json
└── export_test.go # 内部のテストのための非公開のエクスポート
// user_test.go - 同じパッケージ(ホワイトボックステスト)
package user
func TestInternalFunction(t *testing.T) {
// 内部をテストできる
}
// user_external_test.go - 外部パッケージ(ブラックボックステスト)
package user_test
import "myapp/user"
func TestPublicAPI(t *testing.T) {
// 公開APIのみをテスト
}
func TestBasicAssertions(t *testing.T) {
// 等価性
got := Calculate()
want := 42
if got != want {
t.Errorf("got %d, want %d", got, want)
}
// エラーチェック
_, err := Process()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// nil チェック
result := GetResult()
if result == nil {
t.Fatal("expected non-nil result")
}
}
// ヘルパーとしてマーク(スタックトレースに表示されない)
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("unexpected error: %v", err)
}
}
// 使用例
func TestWithHelpers(t *testing.T) {
result, err := Process()
assertNoError(t, err)
assertEqual(t, result.Status, "success")
}
import "reflect"
func assertDeepEqual(t *testing.T, got, want interface{}) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %+v, want %+v", got, want)
}
}
func TestStructEquality(t *testing.T) {
got := User{Name: "Alice", Age: 30}
want := User{Name: "Alice", Age: 30}
assertDeepEqual(t, got, want)
}
// 本番コード
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type UserService struct {
store UserStore
}
// テストコード
type MockUserStore struct {
users map[string]*User
err error
}
func (m *MockUserStore) GetUser(id string) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func (m *MockUserStore) SaveUser(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
// テスト
func TestUserService(t *testing.T) {
mock := &MockUserStore{
users: make(map[string]*User),
}
service := &UserService{store: mock}
// サービスをテスト...
}
// プロダクションコード - 時間を注入可能にする
type TimeProvider interface {
Now() time.Time
}
type RealTime struct{}
func (RealTime) Now() time.Time {
return time.Now()
}
type Service struct {
time TimeProvider
}
// テストコード
type MockTime struct {
current time.Time
}
func (m MockTime) Now() time.Time {
return m.current
}
func TestTimeDependent(t *testing.T) {
mockTime := MockTime{
current: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
}
service := &Service{time: mockTime}
// 固定時間でテスト...
}
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type MockHTTPClient struct {
response *http.Response
err error
}
func (m *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {
return m.response, m.err
}
func TestAPICall(t *testing.T) {
mockClient := &MockHTTPClient{
response: &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{"status":"ok"}`)),
},
}
api := &APIClient{client: mockClient}
// APIクライアントをテスト...
}
func TestHandler(t *testing.T) {
handler := http.HandlerFunc(MyHandler)
req := httptest.NewRequest("GET", "/users/123", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
// ステータスコードをチェック
if rec.Code != http.StatusOK {
t.Errorf("got status %d, want %d", rec.Code, http.StatusOK)
}
// レスポンスボディをチェック
var response map[string]interface{}
if err := json.NewDecoder(rec.Body).Decode(&response); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if response["id"] != "123" {
t.Errorf("got id %v, want 123", response["id"])
}
}
func TestAuthMiddleware(t *testing.T) {
// ダミーハンドラー
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
// ミドルウェアでラップ
handler := AuthMiddleware(nextHandler)
tests := []struct {
name string
token string
wantStatus int
}{
{"valid token", "valid-token", http.StatusOK},
{"invalid token", "invalid", http.StatusUnauthorized},
{"no token", "", http.StatusUnauthorized},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
if tt.token != "" {
req.Header.Set("Authorization", "Bearer "+tt.token)
}
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != tt.wantStatus {
t.Errorf("got status %d, want %d", rec.Code, tt.wantStatus)
}
})
}
}
func TestAPIIntegration(t *testing.T) {
// テストサーバーを作成
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{
"message": "hello",
})
}))
defer server.Close()
// 実際のHTTPリクエストを行う
resp, err := http.Get(server.URL)
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
// レスポンスを検証
var result map[string]string
json.NewDecoder(resp.Body).Decode(&result)
if result["message"] != "hello" {
t.Errorf("got %s, want hello", result["message"])
}
}
func TestUserRepository(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
tests := []struct {
name string
fn func(*testing.T, *sql.DB)
}{
{"create user", testCreateUser},
{"find user", testFindUser},
{"update user", testUpdateUser},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback() // テスト後にロールバック
tt.fn(t, tx)
})
}
}
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("postgres", "postgres://localhost/test")
if err != nil {
t.Fatalf("failed to connect: %v", err)
}
// スキーマを移行
if err := runMigrations(db); err != nil {
t.Fatalf("migrations failed: %v", err)
}
return db
}
func seedTestData(t *testing.T, db *sql.DB) {
t.Helper()
fixtures := []string{
`INSERT INTO users (id, email) VALUES ('1', 'test@example.com')`,
`INSERT INTO posts (id, user_id, title) VALUES ('1', '1', 'Test Post')`,
}
for _, query := range fixtures {
if _, err := db.Exec(query); err != nil {
t.Fatalf("failed to seed data: %v", err)
}
}
}
func BenchmarkCalculation(b *testing.B) {
for i := 0; i < b.N; i++ {
Calculate(100)
}
}
// メモリ割り当てを報告
func BenchmarkWithAllocs(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
ProcessData([]byte("test data"))
}
}
func BenchmarkEncoding(b *testing.B) {
data := generateTestData()
b.Run("json", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
json.Marshal(data)
}
})
b.Run("gob", func(b *testing.B) {
b.ReportAllocs()
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
b.ResetTimer()
for i := 0; i < b.N; i++ {
enc.Encode(data)
buf.Reset()
}
})
}
// 実行: go test -bench=. -benchmem
func BenchmarkStringConcat(b *testing.B) {
b.Run("operator", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "hello" + " " + "world"
}
})
b.Run("fmt.Sprintf", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = fmt.Sprintf("%s %s", "hello", "world")
}
})
b.Run("strings.Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString("hello")
sb.WriteString(" ")
sb.WriteString("world")
_ = sb.String()
}
})
}
func FuzzParseInput(f *testing.F) {
// シードコーパス
f.Add("hello")
f.Add("world")
f.Add("123")
f.Fuzz(func(t *testing.T, input string) {
// パースがパニックしないことを確認
result, err := ParseInput(input)
// エラーがあっても、nilでないか一貫性があることを確認
if err == nil && result == nil {
t.Error("got nil result with no error")
}
})
}
func FuzzJSONParsing(f *testing.F) {
f.Add([]byte(`{"name":"test","age":30}`))
f.Add([]byte(`{"name":"","age":0}`))
f.Fuzz(func(t *testing.T, data []byte) {
var user User
err := json.Unmarshal(data, &user)
// JSONがデコードされる場合、再度エンコードできるべき
if err == nil {
_, err := json.Marshal(user)
if err != nil {
t.Errorf("marshal failed after successful unmarshal: %v", err)
}
}
})
}
# カバレッジを実行してHTMLレポートを生成
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# パッケージごとのカバレッジを表示
go test -cover ./...
# 詳細なカバレッジ
go test -coverprofile=coverage.out -covermode=atomic ./...
// Good: テスタブルなコード
func ProcessData(data []byte) (Result, error) {
if len(data) == 0 {
return Result{}, ErrEmptyData
}
// 各分岐をテスト可能
if isValid(data) {
return parseValid(data)
}
return parseInvalid(data)
}
// 対応するテストが全分岐をカバー
func TestProcessData(t *testing.T) {
tests := []struct {
name string
data []byte
wantErr bool
}{
{"empty data", []byte{}, true},
{"valid data", []byte("valid"), false},
{"invalid data", []byte("invalid"), false},
}
// ...
}
//go:build integration
// +build integration
package myapp_test
import "testing"
func TestDatabaseIntegration(t *testing.T) {
// 実際のDBを必要とするテスト
}
# 統合テストを実行
go test -tags=integration ./...
# 統合テストを除外
go test ./...
import "github.com/testcontainers/testcontainers-go"
func setupPostgres(t *testing.T) *sql.DB {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:15",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "testdb",
},
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
container.Terminate(ctx)
})
// コンテナに接続
// ...
return db
}
func TestParallel(t *testing.T) {
tests := []struct {
name string
fn func(*testing.T)
}{
{"test1", testCase1},
{"test2", testCase2},
{"test3", testCase3},
}
for _, tt := range tests {
tt := tt // ループ変数をキャプチャ
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // このテストを並列実行
tt.fn(t)
})
}
}
func TestWithResourceLimit(t *testing.T) {
// 同時に5つのテストのみ
sem := make(chan struct{}, 5)
tests := generateManyTests()
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
sem <- struct{}{} // 獲得
defer func() { <-sem }() // 解放
tt.fn(t)
})
}
}
# 基本テスト
go test ./...
go test -v ./... # 詳細出力
go test -run TestSpecific ./... # 特定のテストを実行
# カバレッジ
go test -cover ./...
go test -coverprofile=coverage.out ./...
# レースコンディション
go test -race ./...
# ベンチマーク
go test -bench=. ./...
go test -bench=. -benchmem ./...
go test -bench=. -cpuprofile=cpu.prof ./...
# ファジング
go test -fuzz=FuzzTest
# 統合テスト
go test -tags=integration ./...
# JSONフォーマット(CI統合用)
go test -json ./...
# テストタイムアウト
go test -timeout 30s ./...
# 短時間テスト(長時間テストをスキップ)
go test -short ./...
# ビルドキャッシュのクリア
go clean -testcache
go test ./...
// Good: テーブル駆動テストで繰り返しを削減
func TestValidation(t *testing.T) {
tests := []struct {
input string
valid bool
}{
{"valid@email.com", true},
{"invalid-email", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
err := Validate(tt.input)
if (err == nil) != tt.valid {
t.Errorf("Validate(%q) error = %v, want valid = %v",
tt.input, err, tt.valid)
}
})
}
}
// Good: テストデータを testdata/ ディレクトリに配置
func TestLoadConfig(t *testing.T) {
data, err := os.ReadFile("testdata/config.json")
if err != nil {
t.Fatal(err)
}
config, err := ParseConfig(data)
// ...
}
func TestWithCleanup(t *testing.T) {
// リソースを設定
file, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal(err)
}
// クリーンアップを登録(deferに似ているが、サブテストで動作)
t.Cleanup(func() {
os.Remove(file.Name())
})
// テストを続ける...
}
// Bad: 不明確なエラー
if result != expected {
t.Error("wrong result")
}
// Good: コンテキスト付きエラー
if result != expected {
t.Errorf("Calculate(%d) = %d; want %d", input, result, expected)
}
// Better: ヘルパー関数の使用
assertEqual(t, result, expected, "Calculate(%d)", input)
// Bad: 外部状態に依存
func TestBadDependency(t *testing.T) {
result := GetUserFromDatabase("123") // 実際のDBを使用
// テストが壊れやすく遅い
}
// Good: 依存を注入
func TestGoodDependency(t *testing.T) {
mockDB := &MockDatabase{
users: map[string]User{"123": {ID: "123"}},
}
result := GetUser(mockDB, "123")
}
// Bad: テスト間で状態を共有
var sharedCounter int
func TestShared1(t *testing.T) {
sharedCounter++
// テストの順序に依存
}
// Good: 各テストを独立させる
func TestIndependent(t *testing.T) {
counter := 0
counter++
// 他のテストに影響しない
}
// Bad: エラーを無視
func TestIgnoreError(t *testing.T) {
result, _ := Process()
if result != expected {
t.Error("wrong result")
}
}
// Good: エラーをチェック
func TestCheckError(t *testing.T) {
result, err := Process()
if err != nil {
t.Fatalf("Process() error = %v", err)
}
if result != expected {
t.Errorf("got %v, want %v", result, expected)
}
}
| コマンド/パターン | 目的 |
|---|---|
go test ./... |
すべてのテストを実行 |
go test -v |
詳細出力 |
go test -cover |
カバレッジレポート |
go test -race |
レースコンディション検出 |
go test -bench=. |
ベンチマークを実行 |
t.Run() |
サブテスト |
t.Helper() |
テストヘルパー関数 |
t.Parallel() |
テストを並列実行 |
t.Cleanup() |
クリーンアップを登録 |
testdata/ |
テストフィクスチャ用ディレクトリ |
-short |
長時間テストをスキップ |
-tags=integration |
ビルドタグでテストを実行 |
覚えておいてください: 良いテストは高速で、信頼性があり、保守可能で、明確です。複雑さより明確さを目指してください。