【Go言語】TDDで大規模リファクタリング!散らばった環境変数を一箇所にまとめる技術
😱「この設定、どこで使ってるんだっけ…?」
プロジェクトが大きくなるにつれて、こんな悪夢のような状況に陥っていませんか?
// ファイルA: ユーザーサービス
userPoolID := os.Getenv("COGNITO_USER_POOL_ID")
// ファイルB: 認証サービス
poolId := os.Getenv("COGNITO_USER_POOL_ID") // あれ、変数名が違う…
// ファイルC: 共通ライブラリ
cognitoPool := os.Getenv("COGNITO_USER_POOL_ID")
if cognitoPool == "" {
// デフォルト値を設定…でも、他の場所と違う値だったら?
}
設定の呼び出しがコードのあちこちに散らばると、
- 一貫性がない: 同じ設定なのに、違う名前で呼ばれている。
- バグの温床: ある場所では必須チェックをしているのに、別の場所ではしていない。
- メンテナンス地獄: 設定名を一つ変更するのに、何十ファイルも修正する必要がある。
- テストが困難: テストごとに環境変数を設定するのが面倒くさい。
私たちは、自分たちのコードベースを調査し、なんと18箇所にos.Getenvが散らばっていることを発見しました。これは、もはや悪夢です。
このカオスな状況を、今回はTDD(テスト駆動開発)を使って、安全かつ劇的に改善します!
今回のゴール
散らばったos.Getenv呼び出しを、中央集約化された、型安全で、テストしやすいconfigパッケージに生まれ変わらせる!
🚀 TDDは、大規模リファクタリングの羅針盤
今回のような「コードベース全体に影響する大きな変更」こそ、TDDの真価が発揮されます。
- 安全な航海: 既存のテストが、変更によって機能が壊れていないかを常に監視してくれます。
- 明確な目的地: 「テストを先に書く」ことで、どんな
configパッケージを作りたいのか、設計図が明確になります。 - 座礁を防ぐ: リファクタリング中にバグが生まれても、テストが即座に検知してくれます。
さあ、TDDという羅針盤を手に、リファクタリングの大海原へ出発しましょう!
サイクル1:🔴 Red - 「理想の設定管理」をテストで描く
まず、私たちが夢見る「理想のconfigパッケージ」が、どんな風に動いてほしいかをテストコードで表現します。
テスト1: 環境変数を読み込んで、型安全な設定オブジェクトを作ってほしい!
// pkg/config/config_test.go
func TestLoadConfig_Success(t *testing.T) {
// 準備: テスト用の環境変数を設定
os.Setenv("COGNITO_USER_POOL_ID", "ap-northeast-1_TestPool123")
os.Setenv("USERS_TABLE_NAME", "test-users-table")
os.Setenv("DEBUG_LOGGING", "true")
// テストが終わったら環境変数を元に戻す
defer os.Clearenv()
// 実行: 理想の関数 `LoadConfig` を呼び出す(まだ存在しない!)
config, err := LoadConfig()
// 検証:
assert.NoError(t, err) // エラーなく読み込めるはず
assert.NotNil(t, config) // configオブジェクトが作られるはず
// 型安全なゲッターメソッドで値を取得できるはず
assert.Equal(t, "ap-northeast-1_TestPool123", config.GetCognitoUserPoolID())
assert.Equal(t, "test-users-table", config.GetUsersTableName())
assert.True(t, config.IsDebugEnabled()) // bool型として扱える!
}
テスト2: 必須の環境変数がなかったら、ちゃんとエラーを返してほしい!
// pkg/config/config_test.go
func TestLoadConfig_MissingRequiredVariable(t *testing.T) {
// USERS_TABLE_NAME を設定しないでおく
os.Setenv("COGNITO_USER_POOL_ID", "ap-northeast-1_TestPool123")
defer os.Clearenv()
// 実行
_, err := LoadConfig()
// 検証:
assert.Error(t, err) // 必ずエラーになるはず
assert.Contains(t, err.Error(), "USERS_TABLE_NAME") // エラーメッセージに変数名が含まれるはず
}
実行結果:期待通りの「失敗」!
当然、LoadConfigなんて関数はまだないので、コンパイルエラーで失敗します。これでOK!私たちの「設計図」が完成しました。
サイクル2:🟢 Green - テストを通すためだけの最小限のコードを書く
次に、この「設計図(失敗するテスト)」を満たすためだけの、最小限のコードを実装します。
Config構造体を定義する
// pkg/config/config.go
package config
// Config は、アプリケーションの全設定を保持する構造体
type Config struct {
// 必須の設定
CognitoUserPoolID string
CognitoClientID string
UsersTableName string
// オプション(デフォルト値あり)
AWSRegion string
AllowedOrigins string
DebugLogging bool
}
LoadConfig関数を実装する
// pkg/config/config.go
import (
"fmt"
"os"
"strings"
)
// LoadConfig は環境変数から設定を読み込み、検証する
func LoadConfig() (*Config, error) {
config := &Config{}
// 必須の変数を読み込む
config.CognitoUserPoolID = os.Getenv("COGNITO_USER_POOL_ID")
if config.CognitoUserPoolID == "" {
return nil, fmt.Errorf("必須の環境変数が見つかりません: COGNITO_USER_POOL_ID")
}
config.UsersTableName = os.Getenv("USERS_TABLE_NAME")
if config.UsersTableName == "" {
return nil, fmt.Errorf("必須の環境変数が見つかりません: USERS_TABLE_NAME")
}
// ...他の必須変数も同様に...
// オプションの変数を読み込む(なければデフォルト値)
config.AWSRegion = os.Getenv("AWS_REGION")
if config.AWSRegion == "" {
config.AWSRegion = "ap-northeast-1"
}
debugStr := strings.ToLower(os.Getenv("DEBUG_LOGGING"))
config.DebugLogging = (debugStr == "true" || debugStr == "1")
return config, nil
}
型安全なゲッターメソッドを実装する
// pkg/config/config.go
// GetCognitoUserPoolID は、CognitoのユーザープールIDを返す
func (c *Config) GetCognitoUserPoolID() string {
return c.CognitoUserPoolID
}
// IsDebugEnabled は、デバッグログが有効かどうかを返す
func (c *Config) IsDebugEnabled() bool {
return c.DebugLogging
}
// ...他のゲッターも同様に...
実行結果:テストが「成功」!
$ go test -v ./pkg/config
PASS
ok poc-cognite/pkg/config 0.003s
やりました!私たちの「理想のconfigパッケージ」のプロトタイプが完成しました。
サイクル3:🔵 Refactor - 18箇所のos.Getenvを置き換えていく
いよいよ、リファクタリングの本番です。テストという強力な味方がいるので、自信を持って進められます。
コードベースに散らばるos.Getenvを、新しく作ったconfig.LoadConfig()に一つずつ置き換えていきます。
Before: カオスな直接呼び出し
// cmd/update-user/main.go (修正前)
func businessHandler(...) (..., error) {
// ...
tableName := os.Getenv("USERS_TABLE_NAME") // 😭 文字列、検証なし
if tableName == "" {
// ...
}
// ...
}
After: スッキリ中央集約!
// cmd/update-user/main.go (修正後)
import "poc-cognite/pkg/config" // 新しいパッケージをインポート
func businessHandler(...) (..., error) {
// ...
cfg, err := config.LoadConfig() // ✨ 型安全な設定読み込み
if err != nil {
// 起動時に設定ミスがわかる!
return ..., fmt.Errorf("設定の読み込みに失敗: %w", err)
}
tableName := cfg.GetUsersTableName() // ✨ 型安全なゲッター
// ...
}
この地道な作業を、18箇所すべてで繰り返します。変更を加えるたびに、プロジェクト全体のテストを実行して、何も壊していないことを確認します。
$ go test ./...
PASS ✅
この緑のPASSが、私たちの心の平穏を保ってくれます。
🎉 改善の効果は絶大!どう変わった?
📊 定量的な改善
| 項目 | Before (改善前) | After (改善後) |
|---|---|---|
os.Getenv呼び出し数 | 18箇所 | ✨ 1箇所 (configパッケージ内のみ) |
| 設定の検証 | バラバラ、または無し | ✨ 全設定で統一 |
| デフォルト値の管理 | 7箇所に散在 | ✨ 1箇所に集約 |
| テスト容易性 | 低い | ✨ 非常に高い |
🚀 質的な改善
型安全で、もう間違えない!
cfg.IsDebugEnabled()は必ずbool型を返します。もう"true"という文字列と比較する必要はありません。テストが、驚くほど書きやすい! テスト用の設定オブジェクトを作るだけで、どんな状況でも簡単に再現できるようになりました。
設定ミスは、実行時ではなく「起動時」にわかる! 必須の環境変数がなければ、アプリケーションは起動しません。これにより、実行中の予期せぬエラーを未然に防げます。
保守性が爆発的に向上! 新しい設定を追加するのも、
configパッケージを修正するだけ。もうコードベース全体を探し回る必要はありません。


