DATA-DOG/go-txdbでDB接続を含むテストを楽に書こう


DB接続を含むテストはツライ

  • テスト用のデータをテストケースごとに用意しないといけない。
  • DBの変更結果が他のテストケースに影響を与えないようにリセットしないといけない。
  • DBへの変更が他のテストケースに影響を与えるので並列実行できない。

DATA-DOG/go-txdbを使うと改善できる

  • DATA-DOG/go-txdb で生成することのできるDBコネクションには↓のような特徴があります。
    • sql.DBと互換性がある。
    • すべてのクエリが独立したトランザクション内で実行される。
    • .Close()を呼ぶとそのトランザクション内で実行されたクエリがすべてRollbackされる。
  • これをうまく使うと、テストケースごとに独立したトランザクション内でクエリを実行することができ、テスト終了後にDB変更がRollbackされるので、テストケースごとのデータ処理が必要なくなり、他のテストケースへの影響もなくなるのでテストを並列実行することができます。
  • 実際のサンプルコードは こちら に置いています。

軽い解説

pkg/dao/dao_test.go

  • 今回はDB接続を含むコードをdaoパッケージに置きます。
  • TestMainを定義して、テストに必要なデータを go-testfixtures/testfixtures を使って挿入します。
    • testfixturesは平行テストに対応していないことがREADMEにも書いてありますが、今回はTestMainでしか呼ばれないので問題ありません。
  • 後のテストでtxdbを使うためにtxdb.Registerを呼んでおきます。
func TestMain(m *testing.M) {
    prepare()

    txdb.Register("txdb", "mysql", config.DB.DSN)

    code := m.Run()
    os.Exit(code)
}

func prepare() {
    db, err := sql.Open(config.DB.Driver, config.DB.DSN)
    if err != nil {
        panic(err)
    }
    defer db.Close()

    fixtures, err := testfixtures.New(
        testfixtures.Database(db),
        testfixtures.Dialect("mysql"),
        testfixtures.Directory("/go/src/github.com/rinchsan/txdb-todo/testdata/fixtures"),
    )
    if err != nil {
        panic(err)
    }

    if err := fixtures.Load(); err != nil {
        panic(err)
    }
}

pkg/dao/user_test.go

  • ユーザ追加のテストを例に取ります。
    • テスト対象のコードやDBのスキーマなどの詳細はGitHubのほうを見てください。
  • sql.Openに渡すDriverに"txdb"を指定して取得したコネクションを使ってテストをしていきます。
    • sql.Openの第2引数に渡す文字列ごとに独立したトランザクションを作成してくれます。
  • deferで呼んでいるdb.Close()によってテストケースごとにDBの変更がRollbackされています。
  • t.Parallel()を利用してテストを平行に走らせることも可能です。
  • ちなみに今回の例では環境変数を利用することで、ローカル開発用に使っているデータベースとは別のものを使ってテストを行うようにしていますので、詳しくは Makefiletestを見てみてください。
func TestUserImpl_Add(t *testing.T) {
    t.Parallel()

    cases := map[string]struct {
        user  *entity.User
        noErr bool
    }{
        "new user": {
            user:  &entity.User{Username: "rinchsan"},
            noErr: true,
        },
        "duplicate username": {
            user:  &entity.User{Username: "John"},
            noErr: true,
        },
        "empty username": {
            user:  &entity.User{Username: ""},
            noErr: true,
        },
    }

    for name, c := range cases {
        c := c
        t.Run(name, func(t *testing.T) {
            t.Parallel()

            db, err := sql.Open("txdb", uuid.New().String())
            assert.NoError(t, err)
            defer db.Close()
            impl := dao.NewUser(db)

            err = impl.Add(context.Background(), c.user)
            if c.noErr {
                assert.NoError(t, err)
            } else {
                assert.Error(t, err)
            }
        })
    }
}

感想

ビジネスロジックのテストをgomockとかを使って書くと、今回のtxdbと合わせて結構いい感じにプロジェクト全体のテストが書けそうです。