この記事は、Merpay Tech Openness Month 2023 の19日目の記事です。
こんにちは。メルペイのバックエンドエンジニアの@youxkeiと@fivestarです。
前回の記事「Goでテスト用のフィクスチャをいい感じに書く」では、fixtureパッケージを導入することで、テスト用のデータベースのフィクスチャを以下のような点で「いい感じに」記述できるようになりました。
- モデルのIDのセットなどの自明な処理が暗黙的に行われる
- 記述した際のコードのネストがモデルのリレーションを表す
- その際、マッピング用のモデルが必要な場合は暗黙的に用意される
fixtureパッケージを使用することで、テストケースに必要な値をモデルにセットしつつ、モデル間のリレーションがわかりやすい形でフィクスチャを記述することができます。
各モデルに対応するマッピング用の関数はほぼ定形なので、これを自動生成することで汎用的に使うことができそうです。
そこで、モデルとなる構造体一覧からfixtureパッケージを生成するツールyofixtureを作成しました。
yoのジェネレータシステムをベースに実装したので、yoにあやかってツールの名前をyofixtureとしました。ただ、yofixtureはyoで生成したモデル以外でも使用することができます。
yofixtureによるfixtureパッケージの生成
前回の記事と同様に、具体例として以下のような図書館蔵書モデルを考えます。
package models
type Library struct {
LibraryID string
Name string
}
type Book struct {
BookID string
Name string
LibraryID string
}
type Author struct {
AuthorID string
Name string
}
type BookAuthorMapping struct {
BookID string
AuthorID string
}
yofixtureでは、CLIで以下のようなyamlの設定ファイルからfixtureパッケージのソースコードを生成することができます。
models:
- name: Library
relations:
- Book: { LibraryID: LibraryID }
- name: Book
- Author: {}
- name: Author
- name: BookAuthorMapping
設定ファイルでは、モデルとそのリレーションを設定することができます。
ここでは、前回の記事の具体例で使用した図書館蔵書モデルと、LibraryとBook、BookとAuthorのリレーションを定義しています。
LibraryとBookのリレーションについては「Book.LibraryIDにLibrary.LibraryIDをセットする」という形で定義しています。フィールドの値をセットする形であれば、設定ファイルでリレーションを定義できます。
BookとAuthorのリレーションについては、BookAuthorMappingを介したリレーションのため、設定ファイルでは定義できません。
このような複雑なリレーションを実現するために、yofixtureはプロトタイプパターンを用いて既存のリレーションの挙動を変更できるようなコードを生成します。
BookとAuthorのリレーションは、以下のように生成したfixtureパッケージ内でリレーションを定義できます。
package fixture
import (
"testing"
"path/to/models"
)
func init() {
prototype.ConnectToBook = func(tb testing.TB, fixt *Fixture, book *models.Book, connectingModel any) {
tb.Helper()
switch connectingModel := connectingModel.(type) {
case *models.Author:
// BookとAuthorのリレーションの場合、BookAuthorMappingを追加する
fixt.AddBookAuthorMapping(tb, fixt,
prototype.CreateBookAuthorMapping(func(m *models.AddBookAuthorMapping) {
m.BookID = book.BookID
m.AuthorID = connectingModel.AuthorID
}),
)
}
// デフォルトの処理
connectToBook(tb, fixt, book, connectingModel)
}
}
このように、fixtureパッケージに生成されるデフォルトのprototypeを拡張することで、BookからAuthorへのリレーションを張る際の独自の処理を定義することができます。
さらに、prototypeの拡張によって、以下のようにモデルを作成した際のフィールドのデフォルト値を定義することができます。
package fixture
import (
"testing"
"github.com/google/uuid"
"path/to/models"
)
func init() {
prototype.CreateLibrary = func(setters ...func(l *models.Library)) *models.Library {
l := &models.Library{
// デフォルト値をセット
LibraryID: uuid.New().String(),
}
for _, setter := range setters {
setter(l)
}
return l
}
}
yofixtureで生成したfixtureパッケージを使う
生成したfixtureパッケージは、前回の記事と同様に使うことができます。
import (
"testing"
"path/to/fixture"
"path/to/models"
)
func TestListBooksByAuthor(t *testing.T) {
author := fixture.Author(func(a *models.Author) {
a.Name = "夏目漱石"
})
f := fixture.Build(t,
fixture.Library(func(l *models.Library) {
l.Name = "ほげ図書館"
}).Connect(
fixture.Book(func(b *models.Book) {
b.Name = "吾輩は猫である"
}).Connect(author),
fixture.Book(func(b *models.Book) {
b.Name = "こころ"
}).Connect(author), // 同じauthor
),
)
setupDB(t, f.Collect())
// 以下テストコードが続く
}
まとめ
fixtureパッケージを生成するyofixtureを作成しました。
yofixtureは現在社内ツールとして使われていて、オープンソース化も検討しています。ご期待ください!
明日の記事は @nu2 さんです。引き続きお楽しみください。