はじめに
GORMにはLaravelにあるような、DBのリフレッシュが用意されていません。
そのため、DBの単体テストをするためには自分でプログラムを準備する必要があります。
前提
まず単体テストをしたい関数ですが、以下のような全件取得するものサンプルとします。
DDDで作っているので、Structを使っていますがこの記事には関係ありません。
type userPersistence struct {
db *gorm.DB
}
func NewUserPersistence(r Repository) repository.UserRepository {
return &userPersistence{r.GetConn()}
}
func (bp bookPersistence) GetAllBook() ([]entity.Book, error) {
var books []entity.Book
err := bp.db.Find(&books).Error
if err != nil {
log.Error(err.Error())
return nil, errors.New("Get all book failed")
}
return books, nil
}
単体テスト
事前準備
単体テスト本体の紹介の前に、準備用のプログラムです。
ポイントは3つあります。
- NewTestRepositoryで、
DropTable
とAutoMigrate
を使ってDB初期化してます。 -
insertBooks
でテストケース用のデータを投入できるようにしています。 - DBでミリ秒が落ちてしまうので、
convertDBTime
で時間の精度を調整しています。
const location = "Asia/Tokyo"
// NewRepository DBへ接続を行う
func NewRepository(conf config.Config) (Repository, func(), error) {
DBURL := fmt.Sprintf("%s:%s@(%s:%d)/%s?parseTime=true&loc=Asia%%2FTokyo", conf.DB.User, conf.DB.Password, conf.DB.Host, conf.DB.Port, conf.DB.Name)
db, err := gorm.Open("mysql", DBURL)
if err != nil {
return nil, nil, err
}
// DB切断用の関数を定義
cleanup := func() {
if err := db.Close(); err != nil {
log.Print(err)
}
}
return &repositoryStruct{
db: db,
}, cleanup, nil
}
// Automigrate マイグレーション
func (r *repositoryStruct) Automigrate() error {
return r.db.AutoMigrate(&entity.Book{}).Error
}
// Connectionを取得する
func (r *repositoryStruct) GetConn() *gorm.DB {
return r.db
}
// NewTestRepository test用DBを準備する
func NewTestRepository() (Repository, func(), error) {
// test用の設定ファイルを読み込む
conf, err := config.NewConfig()
if err != nil {
return nil, nil, err
}
// test用DBへ接続する
repo, cleanup, err := NewRepository(conf)
if err != nil {
return nil, nil, err
}
// cleanup db for test
err = repo.GetConn().DropTableIfExists(&entity.Book{}).Error
if err != nil {
return nil, nil, err
}
// create tables for test
err = repo.GetConn().AutoMigrate(
entity.Book{},
).Error
if err != nil {
return nil, nil, err
}
return repo, cleanup, nil
}
func seedBooks() ([]entity.Book, error) {
now, err := convertDBTime(now())
if err != nil {
return nil, err
}
books := []entity.Book{
{
Isbn: "9784798121963",
Title: "エリック・エヴァンスのドメイン駆動設計",
Author: "エリック・エヴァンス",
CreatedAt: *now,
UpdatedAt: *now,
},
{
Isbn: "9784873117522",
Title: "Go言語によるWebアプリケーション開発",
Author: "Mat Ryer",
CreatedAt: *now,
UpdatedAt: *now,
},
}
return books, nil
}
func insertBooks(r Repository) ([]entity.Book, error) {
books, err := seedBooks()
if err != nil {
return nil, err
}
for _, v := range books {
err = r.GetConn().Create(&v).Error
if err != nil {
return nil, err
}
}
var insertedBooks []entity.Book
err = r.GetConn().Find(&insertedBooks).Error
if err != nil {
return nil, err
}
return insertedBooks, nil
}
// now timezoneを指定した現在のtimeを返す
func now() time.Time {
loc, err := time.LoadLocation(location)
if err != nil {
log.Fatal(err)
}
return time.Now().In(loc)
}
// convertDBTime ミリ秒を切り捨てる
func convertDBTime(t time.Time) (*time.Time, error) {
loc, err := time.LoadLocation(location)
if err != nil {
log.Fatal(err)
}
const layout = "2006-01-02 15:04:05 -0700 MST"
t, parseErr := time.Parse(layout, t.Format(layout))
if parseErr != nil {
return nil, parseErr
}
t = t.In(loc)
return &t, nil
}
単体テスト本体
単体テストは以下のようになります。
テストケースごとにNewTestRepository()を呼び出すことで、DBをリフレッシュしています。
あとは普通の単体テストになります。
func TestGetAllBook_Success(t *testing.T) {
// テスト用DBを準備
testRepo, cleanup, err := NewTestRepository()
if err != nil {
log.Fatal("faild create test repository:", err)
}
defer cleanup()
// テストデータをInsert
seedBooks, err := insertBooks(testRepo)
if err != nil {
log.Fatal("faild insert data", err)
}
// テスト実行
bp := NewBookPersistence(testRepo)
books, getErr := bp.GetAllBook()
if getErr != nil {
log.Fatal(getErr)
}
// 結果の検証
// Insetされているものが全て返ってきているか
assert.ElementsMatch(t, seedBooks, books)
}
いかがだったでしょうか?
実際に使っているソースなので、関係ないソースが混じってしまっていますが、少しでも皆様の参考になれば幸いです!