結論
- 実装ではなくインターフェースに依存させる
- 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など他にも例があります。