Go Advent Calendar 2022の17日目の記事です。
はじめに
データの取り扱いのテストはどのように実装していますか?
例えば、RDBMSであれば、コネクション単位でロールバックしてくれるような、go-txdb等があるので、割と簡単にテスト単位でデータ制御ができると思いますが、DynamoDBなどのNoSQLにはそのような機能(ライブラリ)がなかったため、testify
のsuite
を使用して同じようなことを行ったので、紹介したいと思います。
testify
testifyは、おそらくGoを使ってテストを書いたことがあれば知っているであろうというくらい有名なライブラリかと思います。
テストを書く際に簡潔かつ便利な機能が備わっていますが、今回は、その中の1つであるsuite-packageを使ってみました。
suite-packageとは、簡潔に言えば、テスト実行時に必要な処理をSetupTest
やTearDownTest
に定義しておくことで、処理の重複を避け、シンプルに記述することができます。
以下原文
The suite package provides functionality that you might be used to from more common object oriented languages. With it, you can build a testing suite as a struct, build setup/teardown methods and testing methods on your struct, and run them with 'go test' as per normal.
中身
suiteパッケージの中身を見てみると、各interfaceが定義されているため、こちらを満たすことで、好きなタイミングで好きな処理を挟むことができます。
https://github.com/stretchr/testify/blob/master/suite/interfaces.go
詳しくは、上記を見ていただければと思いますが、簡単にタイミングについてまとめた表になります。
メソッド | タイミング |
---|---|
SetupSuite | suite実行前 |
SetupTest | 各テスト実行前 |
TearDownSuite | suite実行後 |
TearDownTest | 各テスト実行後 |
BeforeTest | 各テスト実行前 |
AfterTest | 各テスト実行後 |
HandleStats | suite実行後 |
SetupSubTest | 各テスト内のサブテスト実行前 |
TearDownSubTest | 各テスト内のサブテスト実行後 |
実装
実際にsuiteを使って、DynamoDBとのテストを実装します。
上記記載したとおり、interface
を満たしますが、テストの冪等性を保つため、
- 各テストの実行前にテーブルを作成 => SetupTest
- 各テストの実行後にテーブルを削除 => TearDownTest
に処理を実装します。
詳細は割愛しますが、テストにはdynamodb-localを使って、Docker上で操作をしています。
以下に最終的なテストコードを記載します。
詳細説明していきます。
type UserInfo struct {
suite.Suite
db *dynamo.DB
UserID string `dynamo:"ID,hash" index:"Seq-ID-index,range"`
Time time.Time `dynamo:"Time,range"`
UUID string `index:"UUID-index,hash"`
}
func TestUserInfo(t *testing.T) {
// database初期化(dynamoDBLocal)
database.Init()
suite.Run(t, &UserInfo{
db: database.GetDB(),
})
}
func (suite *UserInfo) SetupTest() {
err := suite.db.CreateTable("UserInfo", UserInfo{}).Run()
if err != nil {
fmt.Println(err)
}
}
func (suite *UserInfo) TearDownTest() {
suite.db.Table("UserInfo").DeleteTable().Run()
}
func (suite *UserInfo) TestUserInfo_Insert() {
record := UserInfo{
UserID: "user_id",
Time: time.Now(),
UUID: "uuid",
}
repo := model.UserInfoRepository{}
err := repo.InsertUserInfo(record)
suite.Nil(err)
var gots []map[string]interface{}
err = suite.db.Table("UserInfo").Scan().All(&gots)
suite.Nil(err)
suite.Equal(1, len(gots))
}
func (suite *UserInfo) TestUserInfo_Get() {
records := []UserInfo{
{
UserID: "user_id1",
Time: time.Now(),
UUID: "uuid2",
},
{
UserID: "user_id2",
Time: time.Now(),
UUID: "uuid2",
},
}
for _, v := range records {
err := suite.db.Table("UserInfo").Put(v).Run()
suite.Nil(err)
}
repo := model.UserInfoRepository{}
got, err := repo.GetUserInfo("user_id1")
suite.Nil(err)
suite.True(cmp.Equal(records[0], got))
}
定義と初期化
testに使用するstruct
を定義します。ここで、suite.Suite
の埋め込みを行い、実装を行います。
また、それ以外にtestで使用するフィールドなどを定義しておきます(今回はdynamoDBや、カラム定義)
type UserInfo struct {
suite.Suite
db *dynamo.DB
UserID string `dynamo:"ID,hash" index:"Seq-ID-index,range"`
Time time.Time `dynamo:"Time,range"`
UUID string `index:"UUID-index,hash"`
}
通常のテストと同様にテスト関数を定義します。ここで、suite.Run
をして、先程定義したdbの初期化やDI等、初期処理を行います。
func TestUserInfo(t *testing.T) {
// database初期化(dynamoDBLocal)
database.Init()
suite.Run(t, &UserInfo{
db: database.GetDB(),
})
}
- 各テストの実行前にテーブルを作成 => SetupTest
- 各テストの実行後にテーブルを削除 => TearDownTest
を行いたいので、定義したstruct
に対して、メソッドを定義して、interface
を満たします。
今回は、DynamoDBのテーブル作成と、テーブル削除を都度行います。
func (suite *UserInfo) SetupTest() {
err := suite.db.CreateTable("UserInfo", UserInfo{}).Run()
if err != nil {
fmt.Println(err)
}
}
func (suite *UserInfo) TearDownTest() {
suite.db.Table("UserInfo").DeleteTable().Run()
}
テスト部分
テスト部分は目新しいところは特に無いですが、suite.Suite
が埋め込まれているstruct
をレシーバーとして、テストを定義します。
それぞれのテストが独立しているため、 登録や取得処理において、データの競合等起こることなくテストの実装できます。
func (suite *UserInfo) TestUserInfo_Insert() {
record := UserInfo{
UserID: "user_id1",
Time: time.Now(),
UUID: "uuid1",
}
repo := model.UserInfoRepository{}
err := repo.InsertUserInfo(record)
suite.Nil(err)
var gots []map[string]interface{}
err = suite.db.Table("UserInfo").Scan().All(&gots)
suite.Nil(err)
suite.Equal(1, len(gots))
}
func (suite *UserInfo) TestUserInfo_Get() {
records := []UserInfo{
{
UserID: "user_id1",
Time: time.Now(),
UUID: "uuid1",
},
{
UserID: "user_id2",
Time: time.Now(),
UUID: "uuid2",
},
}
for _, v := range records {
err := suite.db.Table("UserInfo").Put(v).Run()
suite.Nil(err)
}
repo := model.UserInfoRepository{}
got, err := repo.GetUserInfo("user_id1")
suite.Nil(err)
suite.True(cmp.Equal(records[0], got))
}
まとめ
suite-packageを利用することで、可読性が良いテストを書くことができました。
特に、NoSQLなど、データの取り扱いテストに関しては、疎結合に書くことができるため、機会があれば積極的に使っていければ思います。