こんにちは!フリーランスエンジニアのこたろうです。
今回は、ユニットテストを書きやすくするために、クラス間の依存を弱くする方法について解説します。
テスト可能なコードの条件
ユニットテストが書きにくいコードには、共通の特徴があります。それは「外部への依存が強い」ということです。
例えば、以下のようなコードを見てみましょう:
func ProcessOrder(orderID string) error {
// DBから注文情報を取得
order, err := database.GetOrder(orderID)
if err != nil {
return err
}
// 支払い処理
err = payment.ProcessPayment(order.PaymentInfo)
if err != nil {
return err
}
// メール送信
err = mailer.SendOrderConfirmation(order.Email, order)
if err != nil {
return err
}
return nil
}
このコードには以下の問題があります:
-
database.GetOrder
への直接的な依存 -
payment.ProcessPayment
への直接的な依存 -
mailer.SendOrderConfirmation
への直接的な依存
これらの依存があるため、ユニットテストを書く際に以下の課題が発生します:
- 実際のデータベースが必要
- 実際の決済システムが必要
- 実際のメール送信システムが必要
これでは、ユニットテストというより統合テストになってしまいます。
依存を弱くする2つの方法
依存を弱くするには主に2つの方法があります:
- インターフェース化
- 関数の変数化
それぞれ具体的に見ていきましょう。
1. インターフェース化
Goではインターフェースを使って依存を分離できます。
// インターフェース定義
type OrderRepository interface {
GetOrder(orderID string) (*Order, error)
}
type PaymentService interface {
ProcessPayment(paymentInfo PaymentInfo) error
}
type MailService interface {
SendOrderConfirmation(email string, order *Order) error
}
// サービス構造体
type OrderProcessor struct {
orderRepo OrderRepository
paymentSvc PaymentService
mailSvc MailService
}
// コンストラクタ
func NewOrderProcessor(
orderRepo OrderRepository,
paymentSvc PaymentService,
mailSvc MailService,
) *OrderProcessor {
return &OrderProcessor{
orderRepo: orderRepo,
paymentSvc: paymentSvc,
mailSvc: mailSvc,
}
}
// メソッド
func (p *OrderProcessor) ProcessOrder(orderID string) error {
// DBから注文情報を取得
order, err := p.orderRepo.GetOrder(orderID)
if err != nil {
return err
}
// 支払い処理
err = p.paymentSvc.ProcessPayment(order.PaymentInfo)
if err != nil {
return err
}
// メール送信
err = p.mailSvc.SendOrderConfirmation(order.Email, order)
if err != nil {
return err
}
return nil
}
テストコード例
func TestOrderProcessor_ProcessOrder(t *testing.T) {
// モックの作成
mockRepo := &MockOrderRepository{}
mockPayment := &MockPaymentService{}
mockMail := &MockMailService{}
// テスト対象の注文
testOrder := &Order{
ID: "order123",
Email: "user@example.com",
PaymentInfo: PaymentInfo{Total: 1000},
}
// モックの振る舞いを設定
mockRepo.On("GetOrder", "order123").Return(testOrder, nil)
mockPayment.On("ProcessPayment", testOrder.PaymentInfo).Return(nil)
mockMail.On("SendOrderConfirmation", "user@example.com", testOrder).Return(nil)
// サービスの初期化
processor := NewOrderProcessor(mockRepo, mockPayment, mockMail)
// テスト実行
err := processor.ProcessOrder("order123")
// 検証
assert.NoError(t, err)
mockRepo.AssertExpectations(t)
mockPayment.AssertExpectations(t)
mockMail.AssertExpectations(t)
}
2. 関数の変数化
既存のコードを大きく変更せずに依存を弱くする方法として、「関数の変数化」という方法もあります。
// 関数型の定義
type GetOrderFunc func(orderID string) (*Order, error)
type ProcessPaymentFunc func(paymentInfo PaymentInfo) error
type SendMailFunc func(email string, order *Order) error
// 変数として関数を定義し、デフォルト実装を設定
var (
GetOrderFn = database.GetOrder
ProcessPaymentFn = payment.ProcessPayment
SendConfirmationFn = mailer.SendOrderConfirmation
)
// 変更後の関数
func ProcessOrder(orderID string) error {
// 変数経由で関数を呼び出す
order, err := GetOrderFn(orderID)
if err != nil {
return err
}
// 変数経由で関数を呼び出す
err = ProcessPaymentFn(order.PaymentInfo)
if err != nil {
return err
}
// 変数経由で関数を呼び出す
err = SendConfirmationFn(order.Email, order)
if err != nil {
return err
}
return nil
}
テストコード例
func TestProcessOrder(t *testing.T) {
// 元の関数を保存
originalGetOrder := GetOrderFn
originalProcessPayment := ProcessPaymentFn
originalSendConfirmation := SendConfirmationFn
// テスト終了時に元に戻す
defer func() {
GetOrderFn = originalGetOrder
ProcessPaymentFn = originalProcessPayment
SendConfirmationFn = originalSendConfirmation
}()
// テスト用の注文情報
testOrder := &Order{
ID: "order123",
Email: "user@example.com",
PaymentInfo: PaymentInfo{Total: 1000},
}
// モック関数の設定
GetOrderFn = func(orderID string) (*Order, error) {
if orderID == "order123" {
return testOrder, nil
}
return nil, errors.New("order not found")
}
ProcessPaymentFn = func(paymentInfo PaymentInfo) error {
if paymentInfo.Total == 1000 {
return nil
}
return errors.New("payment failed")
}
SendConfirmationFn = func(email string, order *Order) error {
if email == "user@example.com" {
return nil
}
return errors.New("email sending failed")
}
// テスト実行
err := ProcessOrder("order123")
// 検証
assert.NoError(t, err)
}
まとめ:依存を弱くするメリット
- テスタビリティの向上: 外部システムに依存せずにテストできる
- コードの柔軟性: 実装を簡単に入れ替えられる
- 保守性の向上: モジュール間の結合度が下がり、変更の影響範囲が限定される
最初からインターフェースを設計するのは少し手間に感じるかもしれませんが、コードの品質とメンテナンス性という観点では大きなメリットがあります。
特にチーム開発では、このような依存関係の管理が極めて重要です。
テスト可能なコードを書くことは、単にテストを容易にするだけでなく、より良いアーキテクチャへの第一歩でもあります。