LoginSignup
6
0

More than 1 year has passed since last update.

Goのテストライブラリtestifyのsuiteを使ってDynamoDBテストを実装する

Last updated at Posted at 2022-12-16

Go Advent Calendar 2022の17日目の記事です。

はじめに

データの取り扱いのテストはどのように実装していますか?
例えば、RDBMSであれば、コネクション単位でロールバックしてくれるような、go-txdb等があるので、割と簡単にテスト単位でデータ制御ができると思いますが、DynamoDBなどのNoSQLにはそのような機能(ライブラリ)がなかったため、testifysuiteを使用して同じようなことを行ったので、紹介したいと思います。

testify

testifyは、おそらくGoを使ってテストを書いたことがあれば知っているであろうというくらい有名なライブラリかと思います。

テストを書く際に簡潔かつ便利な機能が備わっていますが、今回は、その中の1つであるsuite-packageを使ってみました。

suite-packageとは、簡潔に言えば、テスト実行時に必要な処理をSetupTestTearDownTestに定義しておくことで、処理の重複を避け、シンプルに記述することができます。

以下原文

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など、データの取り扱いテストに関しては、疎結合に書くことができるため、機会があれば積極的に使っていければ思います。

6
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
6
0