1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoでDI(依存性注入)を使った、テスト可能なコードの書き方

Posted at

こんにちは!フリーランスエンジニアのこたろうです。
今回は、ユニットテストを書きやすくするために、クラス間の依存を弱くする方法について解説します。

テスト可能なコードの条件

ユニットテストが書きにくいコードには、共通の特徴があります。それは「外部への依存が強い」ということです。

例えば、以下のようなコードを見てみましょう:

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
}

このコードには以下の問題があります:

  1. database.GetOrderへの直接的な依存
  2. payment.ProcessPaymentへの直接的な依存
  3. mailer.SendOrderConfirmationへの直接的な依存

これらの依存があるため、ユニットテストを書く際に以下の課題が発生します:

  • 実際のデータベースが必要
  • 実際の決済システムが必要
  • 実際のメール送信システムが必要

これでは、ユニットテストというより統合テストになってしまいます。

依存を弱くする2つの方法

依存を弱くするには主に2つの方法があります:

  1. インターフェース化
  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)
}

まとめ:依存を弱くするメリット

  1. テスタビリティの向上: 外部システムに依存せずにテストできる
  2. コードの柔軟性: 実装を簡単に入れ替えられる
  3. 保守性の向上: モジュール間の結合度が下がり、変更の影響範囲が限定される

最初からインターフェースを設計するのは少し手間に感じるかもしれませんが、コードの品質とメンテナンス性という観点では大きなメリットがあります。
特にチーム開発では、このような依存関係の管理が極めて重要です。

テスト可能なコードを書くことは、単にテストを容易にするだけでなく、より良いアーキテクチャへの第一歩でもあります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?