Go: 外部ライブラリを使ったコードをいいかんじにDIしてテストする話

結論

  • 実装ではなくインターフェースに依存させる
  • Goのinterfaceは実装の明示が必要ないので便利

aws-sdk-goのスタブ

外部ライブラリの例としてaws-sdk-goを使ったこういったコード例を考える:

type CertificateFetcher struct {
  client *dynamodb.Client
}

func New(client *dynamodb.Client) *CertificateFetcher {
  return &CertificateFetcher{client: client}
}

func (f *CertificateFetcher) GetCertificate(domain string) (string, error) {
  resp, err := f.client.GetItem(...)
  if err != nil {
    return "", fmt.Errorf("...")
  }
  key := resp.Item["key"]
  if key == nli {
    return "", fmt.Errof("...")
  }
}

DynamoDBと通信させずにエラーのハンドリングや *dynamodb.GetItemOutput から値を取り出す処理などについてテストしたいが、そのためには *dynamodb.Client を差し替える必要がある。

実装を差し替える範囲が広すぎると実際に実行されるコードに対するカバレッジが低くなりテストの意味がなくなるので、適切な境界を見つけてそこで差し替えられるとよさそう。

実際にどうやるか、結論としては CertificateFetcher*dynamodb.Client ではなく必要なメソッドが定義されたinterfaceに依存させるとよい。

例:

type DynamoDBClient interface {
  GetItem(item *dynamodb.GetItemInput) (*dynamodb.GetItemOutput, error)
}

type CertificateFetcher struct {
  client DynamoDBClient
}

func New(client DynamoDBClient) *CertificateFetcher {
  return &CertificateFetcher{client: client}
}

func (f *CertificateFetcher) GetCertificate(domain string) (string, error) {
  resp, err := f.client.GetItem(...)
  if err != nil {
    return "", fmt.Errorf("...")
  }
  key := resp.Item["key"]
  if key == nli {
    return "", fmt.Errof("...")
  }
}

テストコード例:

func TestGetCertificate(t *testing.T) {
  stubClient := &StubDynamoDBClient{}
  fetcher := New(stubClient)
  // ...
}

type StubDynamoDBClient struct {}

func (c *StubDynamoDBClient) GetItem(item *dynamodb.GetItemInput) (*dynamodb.GetItemOuput, error) {
  // ...
}

Goのinterfaceは所与のメソッドを実装していれば特別なアノテーションをつけずとも構造体がinterfaceを実装しているとみなされるので、実装を変更できない外部ライブラリに対してアドホックにinterfaceを定義できるので便利。

このようにインターフェースに依存させてDIしやすくするというのは、たとえばScalaにおけるcake patternなど他にも例があります。