Goでテスト用のフィクスチャを生成する

この記事は、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 さんです。引き続きお楽しみください。

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加