記事の要約
- aws-sdk-go-v2では存在しないiface (interface) を自動で生成したい
- ifacemakerというOSSを使って生成する
- 生成したinterfaceをmockgenと連携する
経緯
aws-sdk-go-v1には${AWS_SERVICE_NAME}iface
というパッケージ名でAWSの各サービス用interfaceが定義されていた。
S3を例に上げるとs3ifaceがあたる。
aws-sdk-go-v1では、この${AWS_SERVICE_NAME}iface
パッケージを使うことでmockgenやStruct Embeddingを組み合わせることでテスト用のmockファイルを簡単に生成できた。
しかしaws-sdk-go-v2になり、とあるversion以降はifaceパッケージが無くなった。
無くなった経緯はdiscussionで記載されているため一度目を通すことをオススメする。
discussionではライブラリ(aws-sdk-go)がinterfaceを公開することのデメリットが記載されている。
要約すると
- ライブラリがinterfaceを用意し、クライアント側でinterfaceを満たす実装をしている場合にクライアント側が壊れてしまう
- 何かしらのversionアップでライブラリに新しくメソッドを追加するとコンパイルエラーになってしまう
- Review Commentsにも依存性を取るためにクライアント側でinterfaceを定義することが推奨として記載されている
aws-sdk-go-v2をmockしテストする方法
unit-testでmockを使用する例はDeveloper Guideに記載されている。
https://aws.github.io/aws-sdk-go-v2/docs/unit-testing/
Developer Guideに記載された方法でmockファイルを作成することはできるが沢山のAWSサービスを利用し、且つ色々なAPIを使用する場合、mockファイルに手数をかけたくないという気持ちがあると思う。
LocalStackでテストできると良いが、有料版を使用していない・LocalStackにまだ存在しないなどmockで代替する場面もある。
そこで、ifacemakerというツールを使って簡易にinterfaceを生成する方法を紹介する。
ifacemaker
ifacemakerはgoファイルに定義された構造体からinterfaceを生成するツール
https://github.com/vburenin/ifacemaker
install
最新版をインストールする場合
go install github.com/vburenin/ifacemaker@latest
ifacemakerを使ってinterfaceを生成する
ifacemaker \
-f "${AWS_MODULE_PATH}/*.go" \
-s "${AWS_STRUCT_NAME}" \
-m "${AWS_MODULE}" \
-p "${OUTPUT_PACKAGE_NAME}" \
-i "${OUTPUT_INTERFACE_NAME}" \
-o "${OUTPUT_FILE}"
コマンドオプション
- -f (file)
- 生成元とする対象のファイル
- -s (struct)
- 生成元のstruct名
- -m (import-module)
- 生成元のpackage名
- -p (pkg)
- 出力先のpackage名
- -i (iface)
- 出力先のinterface名
- -o (output)
- 出力するgoファイル名
他にどんなコマンドオプションがあるのかは、ifacemaker --help
で確認するとよい。
事前にaws-sdk-go-v2の使用しているversionやstruct名、ファイルパスを把握しておく必要があるが、調べるのはそこまで手間ではない。
GitHub UIでのfile検索やgo listコマンドでgo.modから使用しているversionを知ることができる。
cognitoidentityproviderパッケージからinterfaceを生成する場合は以下の手順になる
- aws-sdk-go-v2で使用するserviceのモジュール名を調べる
- github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider
- aws-sdk-go-v2で使用するserviceのAPIが実装されているstruct名を調べる
- Client
- aws-sdk-go-v2で使用するserviceのダウンロードファイルパスを調べる
- /Users/me/go/1.21.5/pkg/mod/github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider@v1.32.0
- 生成後のパッケージ名・interface名・出力のパスを決める
ダウンロードしたcognitoidentityproviderのファイルパスを取得する
go list -m -f '{{.Dir}}' github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider
# ^の出力
/Users/me/go/1.21.5/pkg/mod/github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider@v1.32.0
cognitoidentityproviderからinterfaceを生成する
ifacemaker \
-f '/Users/me/go/1.21.5/pkg/mod/github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider@v1.32.0/*.go' \
-s Client \
-m github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider \
-p cognitoiface \
-i CognitoIDProvider \
-o aws/cognito/cognitoiface/cognitoiface.go
コマンドが成功すると、指定したディレクトリにgoファイルが生成される。(一部のみ抜粋)
// Code generated by ifacemaker; DO NOT EDIT.
package cognitoiface
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
. "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
)
// CognitoIDProvider ...
type CognitoIDProvider interface {
// Options returns a copy of the client configuration.
//
// Callers SHOULD NOT perform mutations on any inner structures within client
// config. Config overrides should instead be made on a per-operation basis through
// functional options.
Options() cognitoidentityprovider.Options
// Adds additional user attributes to the user pool schema. Amazon Cognito
// evaluates Identity and Access Management (IAM) policies in requests for this API
// operation. For this operation, you must use IAM credentials to authorize
// requests, and you must grant yourself the corresponding IAM permission in a
// policy. Learn more
// - Signing Amazon Web Services API Requests (https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html)
// - Using the Amazon Cognito user pools API and user pool endpoints (https://docs.aws.amazon.com/cognito/latest/developerguide/user-pools-API-operations.html)
AddCustomAttributes(ctx context.Context, params *AddCustomAttributesInput, optFns ...func(*Options)) (*AddCustomAttributesOutput, error)
// FYI: 下記行には色々なメソッドが定義されている
}
生成したinterfaceからmockgenでmockファイルを作る
aws-sdk-go-v2のcognitoidentityproviderからinterfaceが生成できたため、今度はmockgenを使いgomock用のファイルを作成する。
//go:generate mockgen -source=cognitoiface.go -package=mock_$GOPACKAGE -destination=../../../mock/aws/cognito/mock_cognitoiface.go -self_package=iface-sample/aws/cognito/$GOPACKAGE
package cognitoiface
go generate ./...
を実行して成功すると、指定したパスにgomock用のファイルが作成される
(一部のみ抜粋)
// Code generated by MockGen. DO NOT EDIT.
// Source: cognitoiface.go
//
// Generated by this command:
//
// mockgen -source=cognitoiface.go -package=mock_cognitoiface -destination=../../../mock/aws/cognito/mock_cognitoiface.go -self_package=iface-sample/aws/cognito/cognitoiface
//
// Package mock_cognitoiface is a generated GoMock package.
package mock_cognitoiface
import (
context "context"
reflect "reflect"
. "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
cognitoidentityprovider "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
gomock "go.uber.org/mock/gomock"
)
// MockCognitoIDProvider is a mock of CognitoIDProvider interface.
type MockCognitoIDProvider struct {
ctrl *gomock.Controller
recorder *MockCognitoIDProviderMockRecorder
}
// MockCognitoIDProviderMockRecorder is the mock recorder for MockCognitoIDProvider.
type MockCognitoIDProviderMockRecorder struct {
mock *MockCognitoIDProvider
}
// NewMockCognitoIDProvider creates a new mock instance.
func NewMockCognitoIDProvider(ctrl *gomock.Controller) *MockCognitoIDProvider {
mock := &MockCognitoIDProvider{ctrl: ctrl}
mock.recorder = &MockCognitoIDProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCognitoIDProvider) EXPECT() *MockCognitoIDProviderMockRecorder {
return m.recorder
}
//
以上がifacemakerでaws-sdk-go-v2のinterfaceを生成し、mockgenを使ってgomock用のファイルを作成する流れである。
シェルスクリプトにする場合
go.modをgo listコマンドと組み合わせることで、どのversionのsdkを使っているかがわかるが、毎回手元で実行するのが手間な場合はシェルスクリプトを作っておくとよい。
記事を書いている際に、github.com/eksctl-io/eksctlにifacemakerを使ってaws-sdk-go-v2のinterfaceを生成するシェルスクリプトを見つけた。
こちらを参考にすると良い。
今回作成したサンプル