[Golang] gorpでDBを使ったユニットテストを書くたったひとつの冴えたやりかた

2020-07-29

(エヴァは見たことないです。2回目)

今の現場で DB を使ったユニットテスト環境を整備させてもらったのでどうやって実現したかを共有します。

ちゃんと記事にする許可はもらってるから大丈夫だ問題ない。

この記事は gorp を使う前提で書きますが、条件さえ整えば他のORMでも実現可能です。実際、最初は SQLBoiler 環境でセットアップしました。 条件というのをこの時点で書いても意味不明だと思うので追って説明します。

目次

概要 overview

DBを使ったテストのやり方を考えるといくつか方法が見つかります。

まず、sqlmock を使って期待するSQLが発行されるかを監視する方法。正確にはDBは使いませんが... 今の現場は前までこれでした。DBを用意しなくてよいのはたしかにメリットですが、SQLが複雑になると何が正なのか把握するのが難しくなりますし、 発行するSQLの順番やスペースの有無とかでテストが落ちるのでじわじわ辛くなってきます。 SQLの差分を追うのは人間様には向いていません。

つづいて、予めテスト用のフィクスチャデータを投入して全てのテストケースで使い回すという方法。これは短期的には楽ですが、 ケースごとにデータの整合性を保つのが大変になってきます。 他のケース用に投入した(あるいは削除した)データのせいで既存のテストが動かなくなるなんてよくあることです。 テストが多くなるほど保守が難しくなります。

今回はテストケースごとにトランザクションを張り、終了後に破棄することでDBを常にクリーンに保つという方法をとります。 ただ、このやり方にも手間や落とし穴があるのでそれを軽減、回避する策を併せて解説していきます。

セットアップ setup

この記事の内容を手元で試したいという方は https://github.com/righ/gorp-tips をクローンして コンテナ起動して入りましょう。

$ docker-compose up # -d
$ # デーモン起動しない場合は別シェルで実行すること
$ docker exec -it gorp /bin/bash
# cd src/

これで準備完了です。

gorp の クエリはテンプレートに書いたほうがわかりやすいと思いました。 を解説するために作ったリポジトリなので必要最低限のコード というわけではありません。他の件でなにかあれば今後も追記するかも。

興味ある方はぜひそちらも是非見てくださいね〜

必要なパーツを準備していく parts

ネスト可能なトランザクション nestable transaction

一口にトランザクションのロールバックによってDBをきれいに保つと言っても、そう簡単にはいきません。 問題になるのは対象関数内でもトランザクションを使っている場合です。

これの対処としてトランザクションをネストさせるようにしました。

備考

実際、Djangoというフレームワークではトランザクション内でトランザクションを発行すると自動的に ネストされます

とはいっても、トランザクションにはネストという機能はないので SAVEPOINT というものを使って擬似的にネストを実現しています。 この辺の詳細な解説は Golang でトランザクションをネストさせるたった一つの冴えたやり方 という記事を書いたので参照してください。(記事タイトルはこれに合わせてみた)

実際のコードは以下のような感じになります。

gorp-tips/src/db/transaction.go
package db

import (
	"strconv"

	"github.com/go-gorp/gorp"
)

type NestableTx struct {
	*gorp.Transaction

	savePoint int
	next      *NestableTx
	resolved  bool
}

func (tx *NestableTx) Begin() (*NestableTx, error) {
	tx.next = &NestableTx{
		Transaction: tx.Transaction,
		savePoint:   tx.savePoint + 1,
	}
	if err := tx.Savepoint("SP" + strconv.Itoa(tx.next.savePoint)); err != nil {
		return nil, err
	}
	return tx.next, nil
}

func (tx *NestableTx) Rollback() error {
	tx.resolved = true
	if tx.savePoint > 0 {
		return tx.RollbackToSavepoint("SP" + strconv.Itoa(tx.savePoint))
	}
	return tx.Transaction.Rollback()
}

func (tx *NestableTx) Commit() error {
	if tx.next != nil && !tx.next.resolved {
		if err := tx.next.Commit(); err != nil {
			return err
		}
	}
	tx.resolved = true

	if tx.savePoint > 0 {
		return tx.ReleaseSavepoint("SP" + strconv.Itoa(tx.savePoint))
	}
	return tx.Transaction.Commit()
}

(軽く解説) NestableTx と名付けた構造体に gorp.Transaction という gorp のトランザクションを埋め込みます。 こいつは gorp.Transaction と同じように振る舞いつつも、すでにトランザクションが開始されている状態で Begin が呼ばれるとトランザクションではなく セーブポイント を新たに作ります。

CommitRollback は セーブポイントの ReleaseRollback に対応させています。

これによりトランザクション内で新たなトランザクションを擬似的に扱うことができるようになります。

ファクトリ factory

テストケースごとにDBが初期化されるのでテストデータは私達が毎回登録する必要がありますが、 必要のないテーブルの依存関係を満たしつつレコードを何件もインサートするのはちょっと辛いですよね。

実は Ruby(Rails) だと factorybot (よくしらない) Pythonだと factoryboy という 呼び出すだけで依存関係を含めてレコードを作ってくれる便利なやつがあります。

しかし、残念ながらGoには今の所そこまでのものはありません。(ないよね?)

factory-go という構造体に自動的に値を入れてくれるっぽいライブラリを見つけたのでこれを使って実現してみようと思います。

src/factories 配下にプログラムを配置していきます。

  • 📁 factories
    • 🗒 factories.go
    • 🗒 jets_factory.go
    • 🗒 pilots_factory.go
package factories

type Fields map[string]interface{}
package factories

import (
	"fmt"

	"github.com/bluele/factory-go/factory"

	"gorp-tips/db"
	"gorp-tips/models"
)

var JetFactory = factory.NewFactory(
	&models.Jet{},
).SeqInt("ID", func(n int) (interface{}, error) {
	return n, nil
}).Attr("Age", func(args factory.Args) (interface{}, error) {
	return uint8(20), nil
}).SeqInt("Name", func(n int) (interface{}, error) {
	return fmt.Sprintf("Jet-%d", n), nil
}).Attr("Color", func(args factory.Args) (interface{}, error) {
	return "White", nil
})

// MakeJet Jetのファクトリを作る
func MakeJet(fields Fields, deps []db.Dependency) (*models.Jet, []db.Dependency) {
	m := JetFactory.MustCreateWithOption(fields).(*models.Jet)
	if m.PilotID == 0 {
		pilot, _deps := MakePilot(nil, nil)
		m.PilotID = pilot.ID
		deps = append(deps, _deps...)
	}
	deps = append(deps, m)
	return m, deps
}
package factories

import (
	"github.com/bluele/factory-go/factory"

	"gorp-tips/db"
	"gorp-tips/models"
)

var PilotFactory = factory.NewFactory(
	&models.Pilot{},
).SeqInt("ID", func(n int) (interface{}, error) {
	return n, nil
}).Attr("Name", func(args factory.Args) (interface{}, error) {
	return "Tester", nil
})

// MakePilot Pilotのファクトリを作る
func MakePilot(fields Fields, deps []db.Dependency) (*models.Pilot, []db.Dependency) {
	m := PilotFactory.MustCreateWithOption(fields).(*models.Pilot)
	deps = append(deps, m)
	return m, deps
}

var LanguageFactory = factory.NewFactory(
	&models.Language{},
).SeqInt("ID", func(n int) (interface{}, error) {
	return n, nil
}).Attr("Language", func(args factory.Args) (interface{}, error) {
	return "English", nil
})

// MakeLanguage Languageのファクトリを作る
func MakeLanguage(fields Fields, deps []db.Dependency) (*models.Language, []db.Dependency) {
	m := LanguageFactory.MustCreateWithOption(fields).(*models.Language)
	deps = append(deps, m)
	return m, deps
}

var PilotLanguageFactory = factory.NewFactory(
	&models.PilotLanguage{},
)

// MakePilotLanguage PilotLanguageのファクトリを作る
func MakePilotLanguage(fields Fields, deps []db.Dependency) (*models.PilotLanguage, []db.Dependency) {
	m := PilotLanguageFactory.MustCreateWithOption(fields).(*models.PilotLanguage)
	if m.PilotID == 0 {
		pilot, _deps := MakePilot(nil, nil)
		m.PilotID = pilot.ID
		deps = append(deps, _deps...)
	}
	if m.LanguageID == 0 {
		lang, _deps := MakeLanguage(nil, nil)
		m.LanguageID = lang.ID
		deps = append(deps, _deps...)
	}
	deps = append(deps, m)
	return m, deps
}

一応使い方を説明しておくと、 factory.NewFactory で構造体と格納する値のルールを指定することで任意の構造体を作ることができます。 が、これだけでは外部キーが解決できません。

そこでこれを更にラップした MakeXXXX という関数を作り、外部キーのフィールドがゼロ値(今回はintなので0)の場合に、 さらに別の MakeYYYY 関数を内部で呼び出すようにしています。 この関数は第1仮引数は初期値、第2仮引数は依存レコード(スライス)を受け取ります。

初期値はファクトリにそのまま引き渡されるからわかるとして、依存レコードとはなんでしょうか?

テーブルによっては外部キーを持ち、別テーブルのレコードを必要とします。 上記の例で言えば pilot_languages (中間テーブル)は pilotslanguages テーブルに依存しているので、 MakePilotLanguagePilotIDLanguageID がない(ゼロ値の)場合、 MakePilotMakeLanguage で作った構造体から抽出した ID をフィールドに格納したあと、 それぞれの deps を上位(MakePilotLanguages)の deps に追加します。

この仕組みにより連鎖的に依存関係を解決します。

この deps が解決した依存レコードであり、 引数として受け取りさらに返却することで最後まで引き継げるようになります。 なお、 deps に含まれる依存レコードの順番は外部キー制約を満たす上で非常に重要なので作る場合は 依存されるほうが先 に入るようにしてあげてください。

備考

実際私が案件に導入したファクトリは 依存レコードを引数としては受け取らず 、ファクトリごとに作った依存レコードを呼び出し側で別々に管理していましたが、 作るべきレコードが増えてくると全部に変数名をつけるのが結構めんどくさいので一つの deps に追記する方式にしてみました。再帰ではないけどちょっと継続渡しっぽい感じですね。

渡す引数は増えますが、多分こちらのほうがなんぼか楽です。

テスト実行関数 run test

テストケースごとにトランザクションを張って初期化したいといっても go の テストケース関数はそんな事情を知らないので私達が対応してあげる必要があります。 そこで、トランザクションを作り終了時に破棄するような関数を RunTest を以下のように定義します。

gorp-tips/src/db/testutils.go
package db

import (
	"context"
	"database/sql"
	"testing"

	"gorp-tips/models"

	"github.com/go-gorp/gorp"
	_ "github.com/go-sql-driver/mysql"
)

// TestingBlock テスト対象のブロック(関数)
type TestingBlock func(ctx context.Context, tx *NestableTx)

// Dependency 依存レコード(自身を含む)
type Dependency interface{}

func initDb(t *testing.T) *gorp.DbMap {
	db, err := sql.Open("mysql", "usr:pw@tcp(testing_mysql:3306)/db")
	if err != nil {
		t.Fatalf("Failed to connect db. %s", err)
	}
	dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "utf8mb4"}}
	models.MapStructsToTables(dbmap)
	return dbmap
}

// RunTest テストを実行する
func RunTest(ctx context.Context, t *testing.T, block TestingBlock, deps ...Dependency) {
	dbmap := initDb(t)
	defer dbmap.Db.Close()
	// トランザクション作成
	tx, err := dbmap.Begin()
	if err != nil {
		t.Errorf("Failed to start transaction. %w", err)
		return
	}
	ntx := &NestableTx{Transaction: tx}
	defer ntx.Rollback()

	// dependencies 投入
	for _, m := range deps {
		if err := ntx.Insert(m); err != nil {
			t.Fatalf("Failed to load dependencies. %s, %+v", err, m)
		}
	}
	// テスト実行
	block(ctx, ntx)
}

関数内では テスト用の dbmap から NestableTx を生成し、終了時に Rollback するように defer に仕込んでおきます。

警告

dbmap の DSN は環境ごとに異なると思うのでよしなに書き換えてください。 通常テスト用のDBを指定することになると思うので、開発で使っているDBを指定しないように注意しましょう。

今回は別途コンテナを作りました。

引数として特筆するべきなのは 3番目の block と 4番目の deps です。 block にはDBの中を参照するような実行処理を関数形式で定義したものを高階関数として指定します。 言葉では伝わりづらいので後で実際のテストコードを見てもらうのが早いでしょう。

deps は先ほど説明したファクトリで作った依存レコードです。実行前の時点ではDBに入っていないただの構造体にすぎず、 引数として受け取って block 実行前にDBに入れてあげることでようやくレコードとして参照できるようになります。 可変長引数にしたのは指定しない場合は省略できるようにしたかったからです。必須ということで普通の引数にしてもいいと思います。

ということで必要なパーツは整いました。

ユニットテスト unit test

テスト対象 target

早速テストを書いていきたいところですが、一応対象の関数というかメソッドを説明しておきます。

今回は以下をテストします。

gorp-tips/src/repositories/jet_repository0.go
package repositories

import (
	"context"
	"strings"

	"github.com/go-gorp/gorp"
	"github.com/labstack/gommon/log"

	"gorp-tips/models"
)

type JetRepository interface {
	GetJets(ctx context.Context, req models.Request) ([]models.Result, error)
}

type jetRepository0 struct {
	exec gorp.SqlExecutor
}

func NewJetRepository0(exec gorp.SqlExecutor) JetRepository {
	return &jetRepository0{
		exec: exec,
	}
}

func (r *jetRepository0) GetJets(ctx context.Context, req models.Request) ([]models.Result, error) {
	query := "SELECT jets.name AS jetName, jets.age AS jetAge, jets.color AS jetColor, pilots.name AS pilotName, languages.language "
	query += "FROM jets "
	query += "JOIN pilots ON pilots.id = jets.pilot_id "
	query += "LEFT JOIN pilot_languages ON pilot_languages.pilot_id = jets.pilot_id "
	query += "LEFT JOIN languages ON languages.id = pilot_languages.language_id "
	conds, variables := makeCondition(req)
	if conds != "" {
		query += "WHERE " + conds
	}
	query += " ORDER BY jets.age, jets.id"
	log.Debug(query)

	var results []models.Result
	if _, err := r.exec.Select(&results, query, variables); err != nil {
		log.Error(err)
		return nil, err
	}
	return results, nil
}

func makeCondition(req models.Request) (string, map[string]interface{}) {
	conds := []string{}
	context := map[string]interface{}{}

	if req.Age > 0 {
		conds = append(conds, "jets.age = :age")
		context["age"] = req.Age
	}
	if req.PilotName != "" {
		conds = append(conds, "pilots.name LIKE :pilot_name")
		context["pilot_name"] = "%" + req.PilotName + "%"
	}
	if req.JetName != "" {
		conds = append(conds, "jets.name LIKE :jet_name")
		context["jet_name"] = "%" + req.JetName + "%"
	}
	if req.Language != "" {
		conds = append(conds, "languages.language = :language")
		context["language"] = req.Language
	}

	return strings.Join(conds, " AND "), context
}

処理自体には特に大きな意味はなく適当に結合してフィルタしたレコードを返却するだけの関数ですが、 リポジトリ構造体には多少の工夫があります。このリポジトリ構造体はコネクションを exec フィールドに持ち、 NewJetRepository0 関数によって初期化されます。 引数に渡すのは DBコネクションに相当する構造体になりますが、型は gorp.SqlExecutor というインタフェースです。

通常は dbmap が与えられ、テスト時は 先程説明した ネスト可能なトランザクション NestableTx が指定されます。 これらは異なる型のためインタフェースのような抽象的な型でないと受けることができません。 gorp の場合はそれが gorp.SqlExecutor だったというわけです。

備考

  • SQLBoiler の場合は boil.ContextExecutor

  • Gorm の場合は gorm.DB かな? (使ってないから不明)

冒頭で言った条件というのはここのことで、コネクションとトランザクションを抽象化して扱えるような機構がORMに用意されているかどうかがポイントです。 ここにないORMは君の目でたしかみてみろ!

テストコード test code

おまたせしました。ようやくユニットテストのコードです。

  • 📁 src
    • 📁 controllers
      • 🗒 jet_controller.go
    • 📁 db
      • 🗒 sql.go
      • 🗒 sql2.go
      • 📁 statik
        • 🗒 dummy.go
        • 🗒 statik.go
      • 🗒 testutils.go
      • 🗒 transaction.go
    • 📁 factories
      • 🗒 factories.go
      • 🗒 jets_factory.go
      • 🗒 pilots_factory.go
    • 🗒 go.mod
    • 🗒 go.sum
    • 🗒 main_with_template.go
    • 🗒 main_with_template_in_bin.go
    • 🗒 main_without_template.go
    • 📁 models
      • 🗒 jet_model.go
      • 🗒 request.go
      • 🗒 result.go
    • 📁 repositories
      • 🗒 jet_repository0.go
      • 🗒 jet_repository1.go
      • 🗒 jet_repository2.go
      • 🗒 jet_repository_test.go
    • 📁 sql
      • 🗒 query.sql
package controllers

import (
	"context"

	_ "github.com/go-sql-driver/mysql"

	"gorp-tips/models"
	"gorp-tips/repositories"
)

func GetJets(ctx context.Context, repo repositories.JetRepository, req models.Request) ([]models.Result, error) {
	return repo.GetJets(ctx, req)
}
package db

import (
	"bytes"
	"text/template"
)

func GetSQL(filename string, req interface{}) string {
	var buf bytes.Buffer
	t := template.Must(template.ParseFiles("sql/" + filename))
	t.Execute(&buf, req)
	return buf.String()
}
package db

import (
	"bytes"
	"io/ioutil"
	"os"
	"text/template"

	_ "gorp-tips/db/statik"

	"github.com/rakyll/statik/fs"
)

//go:generate statik -f -src=../sql -m

var files, _ = fs.New()

func GetSQL2(filename string, req interface{}) string {
	var buf bytes.Buffer
	f, _ := files.Open(string(os.PathSeparator) + filename)
	b, _ := ioutil.ReadAll(f)
	t := template.Must(template.New(filename).Parse(string(b)))
	t.Execute(&buf, req)
	return buf.String()
}
package statik
// Code generated by statik. DO NOT EDIT.

package statik

import (
	"github.com/rakyll/statik/fs"
)


func init() {
	data := "PK\x03\x04\x14\x00\x08\x00\x08\x00\x00\x00!(\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00	\x00	\x00query.sqlUT\x05\x00\x01\x80Cm8|\x91?O\xc30\x14\xc4w\x7f\x8aS\xe76b.\xea\x10ZW\xb4\x84\x04\xb9A\x88\xa9\xb2\xc8\xc3r\x95&\x88\xc2\x14\xf5\xbb#;\xf1\x9f\x02\xcdv\xbe\xbc\xfb\xc5\xef\xbc\xe3\x19_\x96\x0c8\xd0\xd7)i\xe4\x91\x90\xee\xcc!\x97G\x9a:_*g\xa7*\xb8om\xdd~\x0e\xfe\xd2h\xf3\xe5C\xd7mD\xb2G\xc7\xaae\xa3\xbe\xa5\xa2S\xe2\x14[\x8b\xe2\xd1\xd2\xd8\xb6\xd8\xe4C\x1a\x85S\x89\xae\xb0\xe8\xfff\x8d\xbd\xaeX\xc6\xd7%\xc2\xf4\xdeS},X>5B\xb9\xc8\x87\xa4\xcd\xfc\xc69e\x08/\xf7\\p\x94\xe2\x993\xa0\xeb\xa0\xdf\xd1\x10\x92T\x11n0;\x9f\x19\x90\xe6\xabP\xe0\x02s\xb3\xb1\x99\x9d\x81\x9a\xcaNt\xdd\x10{rEa2\x89\xd2q\x9d\xd9\xe6\x81c\xde_\xc9\x18\xd7P[\xfa\x07\x14\xde\xb7\xc7\x1ch\x1c\x92\x0d\x9b^R\xfe6a\xd6\xf2\xaf\x19\xb3\n\xb1\xe2\x02w\xaf\xbe\x80i\xaft\xc5n\xd9O\x00\x00\x00\xff\xffPK\x07\x08%R\xf1\xa6\xfb\x00\x00\x00w\x02\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00\x00\x00!(%R\xf1\xa6\xfb\x00\x00\x00w\x02\x00\x00	\x00	\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00query.sqlUT\x05\x00\x01\x80Cm8PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00@\x00\x00\x00;\x01\x00\x00\x00\x00"
		fs.Register(data)
	}
	
package db

import (
	"context"
	"database/sql"
	"testing"

	"gorp-tips/models"

	"github.com/go-gorp/gorp"
	_ "github.com/go-sql-driver/mysql"
)

// TestingBlock テスト対象のブロック(関数)
type TestingBlock func(ctx context.Context, tx *NestableTx)

// Dependency 依存レコード(自身を含む)
type Dependency interface{}

func initDb(t *testing.T) *gorp.DbMap {
	db, err := sql.Open("mysql", "usr:pw@tcp(testing_mysql:3306)/db")
	if err != nil {
		t.Fatalf("Failed to connect db. %s", err)
	}
	dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "utf8mb4"}}
	models.MapStructsToTables(dbmap)
	return dbmap
}

// RunTest テストを実行する
func RunTest(ctx context.Context, t *testing.T, block TestingBlock, deps ...Dependency) {
	dbmap := initDb(t)
	defer dbmap.Db.Close()
	// トランザクション作成
	tx, err := dbmap.Begin()
	if err != nil {
		t.Errorf("Failed to start transaction. %w", err)
		return
	}
	ntx := &NestableTx{Transaction: tx}
	defer ntx.Rollback()

	// dependencies 投入
	for _, m := range deps {
		if err := ntx.Insert(m); err != nil {
			t.Fatalf("Failed to load dependencies. %s, %+v", err, m)
		}
	}
	// テスト実行
	block(ctx, ntx)
}
package db

import (
	"strconv"

	"github.com/go-gorp/gorp"
)

type NestableTx struct {
	*gorp.Transaction

	savePoint int
	next      *NestableTx
	resolved  bool
}

func (tx *NestableTx) Begin() (*NestableTx, error) {
	tx.next = &NestableTx{
		Transaction: tx.Transaction,
		savePoint:   tx.savePoint + 1,
	}
	if err := tx.Savepoint("SP" + strconv.Itoa(tx.next.savePoint)); err != nil {
		return nil, err
	}
	return tx.next, nil
}

func (tx *NestableTx) Rollback() error {
	tx.resolved = true
	if tx.savePoint > 0 {
		return tx.RollbackToSavepoint("SP" + strconv.Itoa(tx.savePoint))
	}
	return tx.Transaction.Rollback()
}

func (tx *NestableTx) Commit() error {
	if tx.next != nil && !tx.next.resolved {
		if err := tx.next.Commit(); err != nil {
			return err
		}
	}
	tx.resolved = true

	if tx.savePoint > 0 {
		return tx.ReleaseSavepoint("SP" + strconv.Itoa(tx.savePoint))
	}
	return tx.Transaction.Commit()
}
package factories

type Fields map[string]interface{}
package factories

import (
	"fmt"

	"github.com/bluele/factory-go/factory"

	"gorp-tips/db"
	"gorp-tips/models"
)

var JetFactory = factory.NewFactory(
	&models.Jet{},
).SeqInt("ID", func(n int) (interface{}, error) {
	return n, nil
}).Attr("Age", func(args factory.Args) (interface{}, error) {
	return uint8(20), nil
}).SeqInt("Name", func(n int) (interface{}, error) {
	return fmt.Sprintf("Jet-%d", n), nil
}).Attr("Color", func(args factory.Args) (interface{}, error) {
	return "White", nil
})

// MakeJet Jetのファクトリを作る
func MakeJet(fields Fields, deps []db.Dependency) (*models.Jet, []db.Dependency) {
	m := JetFactory.MustCreateWithOption(fields).(*models.Jet)
	if m.PilotID == 0 {
		pilot, _deps := MakePilot(nil, nil)
		m.PilotID = pilot.ID
		deps = append(deps, _deps...)
	}
	deps = append(deps, m)
	return m, deps
}
package factories

import (
	"github.com/bluele/factory-go/factory"

	"gorp-tips/db"
	"gorp-tips/models"
)

var PilotFactory = factory.NewFactory(
	&models.Pilot{},
).SeqInt("ID", func(n int) (interface{}, error) {
	return n, nil
}).Attr("Name", func(args factory.Args) (interface{}, error) {
	return "Tester", nil
})

// MakePilot Pilotのファクトリを作る
func MakePilot(fields Fields, deps []db.Dependency) (*models.Pilot, []db.Dependency) {
	m := PilotFactory.MustCreateWithOption(fields).(*models.Pilot)
	deps = append(deps, m)
	return m, deps
}

var LanguageFactory = factory.NewFactory(
	&models.Language{},
).SeqInt("ID", func(n int) (interface{}, error) {
	return n, nil
}).Attr("Language", func(args factory.Args) (interface{}, error) {
	return "English", nil
})

// MakeLanguage Languageのファクトリを作る
func MakeLanguage(fields Fields, deps []db.Dependency) (*models.Language, []db.Dependency) {
	m := LanguageFactory.MustCreateWithOption(fields).(*models.Language)
	deps = append(deps, m)
	return m, deps
}

var PilotLanguageFactory = factory.NewFactory(
	&models.PilotLanguage{},
)

// MakePilotLanguage PilotLanguageのファクトリを作る
func MakePilotLanguage(fields Fields, deps []db.Dependency) (*models.PilotLanguage, []db.Dependency) {
	m := PilotLanguageFactory.MustCreateWithOption(fields).(*models.PilotLanguage)
	if m.PilotID == 0 {
		pilot, _deps := MakePilot(nil, nil)
		m.PilotID = pilot.ID
		deps = append(deps, _deps...)
	}
	if m.LanguageID == 0 {
		lang, _deps := MakeLanguage(nil, nil)
		m.LanguageID = lang.ID
		deps = append(deps, _deps...)
	}
	deps = append(deps, m)
	return m, deps
}
module gorp-tips

go 1.14

require (
	github.com/bluele/factory-go v0.0.0-20200430111232-df9c4ffc2e3e
	github.com/go-gorp/gorp v2.2.0+incompatible
	github.com/go-sql-driver/mysql v1.5.0
	github.com/google/go-cmp v0.5.1
	github.com/labstack/gommon v0.3.0
	github.com/poy/onpar v1.0.0 // indirect
	github.com/rakyll/statik v0.1.7
)
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bluele/factory-go v0.0.0-20200430111232-df9c4ffc2e3e h1:jEw5WGmc8WiBfPb+XxavfbxPAgtPdIycxSjMN8svHzw=
github.com/bluele/factory-go v0.0.0-20200430111232-df9c4ffc2e3e/go.mod h1:C+/xfXxCR66wsm6I3Mzbf72W/Lz2NPsGQhSWDVBa5YU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gorp/gorp v2.2.0+incompatible h1:xAUh4QgEeqPPhK3vxZN+bzrim1z5Av6q837gtjUlshc=
github.com/go-gorp/gorp v2.2.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU=
github.com/poy/onpar v1.0.0 h1:MfdQ9bnas+J1si8vUHAABXKxqOqDVaH4T3LRDYYv5Lo=
github.com/poy/onpar v1.0.0/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ=
github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
package main

import (
	"context"
	"database/sql"
	"flag"
	"fmt"
	"gorp-tips/controllers"
	"gorp-tips/models"
	"gorp-tips/repositories"

	"github.com/go-gorp/gorp"
	_ "github.com/go-sql-driver/mysql"
	"github.com/labstack/gommon/log"
)

func initDb() *gorp.DbMap {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql:3306)/db")
	dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "utf8mb4"}}
	models.MapStructsToTables(dbmap)
	return dbmap
}

func main() {
	dbmap := initDb()
	defer dbmap.Db.Close()

	var (
		age       = flag.Int("age", 0, "jet age")
		pilotName = flag.String("pilot_name", "", "pilot name")
		jetName   = flag.String("jet_name", "", "jet name")
		language  = flag.String("language", "", "language")
	)
	flag.Parse()
	req := models.Request{
		Age:       *age,
		PilotName: *pilotName,
		JetName:   *jetName,
		Language:  *language,
	}
	repo := repositories.NewJetRepository1(dbmap)
	results, err := controllers.GetJets(context.Background(), repo, req)
	if err != nil {
		log.Fatal(err)
		return
	}
	for _, record := range results {
		fmt.Printf("%+v\n", record)
	}
}
package main

import (
	"context"
	"database/sql"
	"flag"
	"fmt"

	"github.com/go-gorp/gorp"
	_ "github.com/go-sql-driver/mysql"
	"github.com/labstack/gommon/log"

	"gorp-tips/controllers"
	"gorp-tips/models"
	"gorp-tips/repositories"
)

func initDb() *gorp.DbMap {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql:3306)/db")
	dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "utf8mb4"}}
	models.MapStructsToTables(dbmap)
	return dbmap
}

func main() {
	dbmap := initDb()
	defer dbmap.Db.Close()

	var (
		age       = flag.Int("age", 0, "jet age")
		pilotName = flag.String("pilot_name", "", "pilot name")
		jetName   = flag.String("jet_name", "", "jet name")
		language  = flag.String("language", "", "language")
	)
	flag.Parse()
	req := models.Request{
		Age:       *age,
		PilotName: *pilotName,
		JetName:   *jetName,
		Language:  *language,
	}
	repo := repositories.NewJetRepository2(dbmap)
	results, err := controllers.GetJets(context.Background(), repo, req)
	if err != nil {
		log.Fatal(err)
		return
	}
	for _, record := range results {
		fmt.Printf("%+v\n", record)
	}

}
package main

import (
	"context"
	"database/sql"
	"flag"
	"fmt"
	"gorp-tips/controllers"
	"gorp-tips/models"
	"gorp-tips/repositories"

	"github.com/go-gorp/gorp"
	_ "github.com/go-sql-driver/mysql"
	"github.com/labstack/gommon/log"
)

func initDb() *gorp.DbMap {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql:3306)/db")
	dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{Engine: "InnoDB", Encoding: "utf8mb4"}}
	models.MapStructsToTables(dbmap)
	return dbmap
}

func main() {
	dbmap := initDb()
	defer dbmap.Db.Close()

	var (
		age       = flag.Int("age", 0, "jet age")
		pilotName = flag.String("pilot_name", "", "pilot name")
		jetName   = flag.String("jet_name", "", "jet name")
		language  = flag.String("language", "", "language")
	)
	flag.Parse()
	req := models.Request{
		Age:       *age,
		PilotName: *pilotName,
		JetName:   *jetName,
		Language:  *language,
	}
	repo := repositories.NewJetRepository0(dbmap)
	results, err := controllers.GetJets(context.Background(), repo, req)
	if err != nil {
		log.Fatal(err)
		return
	}
	for _, record := range results {
		fmt.Printf("%+v\n", record)
	}

}
package models

import (
	"github.com/go-gorp/gorp"
)

type Jet struct {
	ID      int    `db:"id"`
	PilotID int    `db:"pilot_id"`
	Age     uint8  `db:"age"`
	Name    string `db:"name"`
	Color   string `db:"color"`
}

type Pilot struct {
	ID   int    `db:"id"`
	Name string `db:"name"`
}

type Language struct {
	ID       int    `db:"id"`
	Language string `db:"language"`
}

type PilotLanguage struct {
	PilotID    int `db:"pilot_id"`
	LanguageID int `db:"language_id"`
}

// MapStructsToTables 構造体と物理テーブルの紐付け
func MapStructsToTables(dbmap *gorp.DbMap) {
	dbmap.AddTableWithName(Pilot{}, "pilots")
	dbmap.AddTableWithName(Jet{}, "jets")
	dbmap.AddTableWithName(Language{}, "languages")
	dbmap.AddTableWithName(PilotLanguage{}, "pilot_languages")
}
package models

type Request struct {
	Age       int
	PilotName string
	JetName   string
	Language  string
}
package models

type Result struct {
	JetName   string
	JetAge    uint8
	JetColor  string
	PilotName string
	Language  *string
}
package repositories

import (
	"context"
	"strings"

	"github.com/go-gorp/gorp"
	"github.com/labstack/gommon/log"

	"gorp-tips/models"
)

type JetRepository interface {
	GetJets(ctx context.Context, req models.Request) ([]models.Result, error)
}

type jetRepository0 struct {
	exec gorp.SqlExecutor
}

func NewJetRepository0(exec gorp.SqlExecutor) JetRepository {
	return &jetRepository0{
		exec: exec,
	}
}

func (r *jetRepository0) GetJets(ctx context.Context, req models.Request) ([]models.Result, error) {
	query := "SELECT jets.name AS jetName, jets.age AS jetAge, jets.color AS jetColor, pilots.name AS pilotName, languages.language "
	query += "FROM jets "
	query += "JOIN pilots ON pilots.id = jets.pilot_id "
	query += "LEFT JOIN pilot_languages ON pilot_languages.pilot_id = jets.pilot_id "
	query += "LEFT JOIN languages ON languages.id = pilot_languages.language_id "
	conds, variables := makeCondition(req)
	if conds != "" {
		query += "WHERE " + conds
	}
	query += " ORDER BY jets.age, jets.id"
	log.Debug(query)

	var results []models.Result
	if _, err := r.exec.Select(&results, query, variables); err != nil {
		log.Error(err)
		return nil, err
	}
	return results, nil
}

func makeCondition(req models.Request) (string, map[string]interface{}) {
	conds := []string{}
	context := map[string]interface{}{}

	if req.Age > 0 {
		conds = append(conds, "jets.age = :age")
		context["age"] = req.Age
	}
	if req.PilotName != "" {
		conds = append(conds, "pilots.name LIKE :pilot_name")
		context["pilot_name"] = "%" + req.PilotName + "%"
	}
	if req.JetName != "" {
		conds = append(conds, "jets.name LIKE :jet_name")
		context["jet_name"] = "%" + req.JetName + "%"
	}
	if req.Language != "" {
		conds = append(conds, "languages.language = :language")
		context["language"] = req.Language
	}

	return strings.Join(conds, " AND "), context
}
package repositories

import (
	"context"

	"github.com/go-gorp/gorp"
	"github.com/labstack/gommon/log"

	"gorp-tips/db"
	"gorp-tips/models"
)

type jetRepository1 struct {
	exec gorp.SqlExecutor
}

func NewJetRepository1(exec gorp.SqlExecutor) JetRepository {
	return &jetRepository1{
		exec: exec,
	}
}

func (r *jetRepository1) GetJets(ctx context.Context, req models.Request) ([]models.Result, error) {
	query := db.GetSQL("query.sql", req)
	log.Debug(query)

	var results []models.Result
	if _, err := r.exec.Select(&results, query, map[string]interface{}{
		"age":        req.Age,
		"pilot_name": "%" + req.PilotName + "%",
		"jet_name":   "%" + req.JetName + "%",
		"language":   req.Language,
	}); err != nil {
		log.Error(err)
		return nil, err
	}
	return results, nil
}
package repositories

import (
	"context"

	"github.com/go-gorp/gorp"
	"github.com/labstack/gommon/log"

	"gorp-tips/db"
	"gorp-tips/models"
)

type jetRepository2 struct {
	exec gorp.SqlExecutor
}

func NewJetRepository2(exec gorp.SqlExecutor) JetRepository {
	return &jetRepository2{
		exec: exec,
	}
}

func (r *jetRepository2) GetJets(ctx context.Context, req models.Request) ([]models.Result, error) {
	query := db.GetSQL2("query.sql", req)
	log.Debug(query)

	var results []models.Result
	if _, err := r.exec.Select(&results, query, map[string]interface{}{
		"age":        req.Age,
		"pilot_name": "%" + req.PilotName + "%",
		"jet_name":   "%" + req.JetName + "%",
		"language":   req.Language,
	}); err != nil {
		log.Error(err)
		return nil, err
	}
	return results, nil
}
package repositories_test

import (
	"context"
	"testing"

	"github.com/google/go-cmp/cmp"

	"gorp-tips/db"
	"gorp-tips/factories"
	"gorp-tips/models"
	"gorp-tips/repositories"
)

func TestRepository(t *testing.T) {
	var deps []db.Dependency

	ken, deps := factories.MakePilot(factories.Fields{"Name": "Ken"}, deps)
	kyle, deps := factories.MakePilot(factories.Fields{"Name": "Kyle"}, deps)
	kim, deps := factories.MakePilot(factories.Fields{"Name": "Kim"}, deps)

	jp, deps := factories.MakeLanguage(factories.Fields{"Language": "Japanese"}, deps)
	en, deps := factories.MakeLanguage(factories.Fields{"Language": "English"}, deps)
	kr, deps := factories.MakeLanguage(factories.Fields{"Language": "Korean"}, deps)

	_, deps = factories.MakePilotLanguage(factories.Fields{"PilotID": ken.ID, "LanguageID": jp.ID}, deps)
	_, deps = factories.MakePilotLanguage(factories.Fields{"PilotID": kyle.ID, "LanguageID": jp.ID}, deps)
	_, deps = factories.MakePilotLanguage(factories.Fields{"PilotID": kyle.ID, "LanguageID": en.ID}, deps)
	_, deps = factories.MakePilotLanguage(factories.Fields{"PilotID": kim.ID, "LanguageID": jp.ID}, deps)
	_, deps = factories.MakePilotLanguage(factories.Fields{"PilotID": kim.ID, "LanguageID": kr.ID}, deps)

	falcon, deps := factories.MakeJet(factories.Fields{"Age": uint8(40), "Name": "Falcon", "PilotID": ken.ID}, deps)
	hawk, deps := factories.MakeJet(factories.Fields{"Age": uint8(30), "Name": "Hawk", "PilotID": kyle.ID}, deps)
	swallow, deps := factories.MakeJet(factories.Fields{"Age": uint8(20), "Name": "Swallow", "PilotID": kyle.ID}, deps)
	dove, deps := factories.MakeJet(factories.Fields{"Age": uint8(10), "Name": "Dove", "Color": "gray"}, deps)
	eagle, deps := factories.MakeJet(factories.Fields{"Age": uint8(10), "Name": "Eagle", "PilotID": kim.ID}, deps)

	cases := []struct {
		name     string
		req      models.Request
		expected []models.Result
	}{
		{
			name: "age filter",
			req:  models.Request{Age: 10},
			expected: []models.Result{
				{JetName: dove.Name, JetAge: 10, JetColor: dove.Color, PilotName: "Tester", Language: nil},
				{JetName: eagle.Name, JetAge: 10, JetColor: eagle.Color, PilotName: kim.Name, Language: &jp.Language},
				{JetName: eagle.Name, JetAge: 10, JetColor: eagle.Color, PilotName: kim.Name, Language: &kr.Language},
			},
		},
		{
			name: "pilot name filter",
			req:  models.Request{PilotName: "en"},
			expected: []models.Result{
				{JetName: falcon.Name, JetAge: falcon.Age, JetColor: falcon.Color, PilotName: ken.Name, Language: &jp.Language},
			},
		},
		{
			name: "jet name filter",
			req:  models.Request{JetName: "awk"},
			expected: []models.Result{
				{JetName: hawk.Name, JetAge: hawk.Age, JetColor: hawk.Color, PilotName: kyle.Name, Language: &jp.Language},
				{JetName: hawk.Name, JetAge: hawk.Age, JetColor: hawk.Color, PilotName: kyle.Name, Language: &en.Language},
			},
		},
		{
			name: "language filter",
			req:  models.Request{Language: "English"},
			expected: []models.Result{
				{JetName: swallow.Name, JetAge: swallow.Age, JetColor: swallow.Color, PilotName: kyle.Name, Language: &en.Language},
				{JetName: hawk.Name, JetAge: hawk.Age, JetColor: hawk.Color, PilotName: kyle.Name, Language: &en.Language},
			},
		},
	}

	db.RunTest(context.Background(), t, func(ctx context.Context, ntx *db.NestableTx) {
		repo := repositories.NewJetRepository(ntx)
		for _, c := range cases {
			t.Run("GetJets "+c.name, func(t *testing.T) {
				got, err := repo.GetJets(ctx, c.req)
				if err != nil {
					t.Error(err)
					return
				}
				if r := cmp.Diff(got, c.expected); r != "" {
					t.Errorf("failed. expected: %v, got: %v", c.expected, got)
				}
			})
		}
	}, deps...)
}
SELECT
  jets.name AS jetName,
  jets.age AS jetAge,
  jets.color AS jetColor,
  pilots.name AS pilotName,
  languages.language
FROM jets
JOIN pilots ON pilots.id = jets.pilot_id
LEFT JOIN pilot_languages ON pilot_languages.pilot_id = jets.pilot_id
LEFT JOIN languages ON languages.id = pilot_languages.language_id
WHERE TRUE
  {{ if ne .Age 0 -}}
  AND jets.age = :age
  {{- end}}
  {{if ne .PilotName "" -}}
  AND pilots.name LIKE :pilot_name
  {{- end}}
  {{if ne .JetName "" -}}
  AND jets.name LIKE :jet_name
  {{- end}}
  {{if ne .Language "" -}}
  AND languages.language = :language
  {{- end}}
ORDER BY jets.age, jets.id
;

今までのパーツを全部つなぎ合わせただけですが順に説明していきます。

関数の先頭で依存レコードの入れ物 deps を作ります。

その後テストケースで利用するフィクスチャを作るべく それぞれの MakeXXXX を呼び出します。

今回は複数の条件が指定できるので条件ごとにサブケースとしてテーブルテストを実行していこうと思います。ここでサブケースと言っているのが cases 変数です。 検索条件と期待結果をフィールドに持つ構造体のスライスです。まぁ見ればわかりますね。

最後に RunTest です。先程お茶を濁した block (第3引数)がここで活躍します。 実行するテスト処理がずらっと書かれているのがわかりますか? この block は ctx (割とどうでもいい) と ntx が指定されます。

ntx を repo := repositories.NewJetRepository(ntx) のように与えることで、この repo はトランザクションにアクセスできるようになりました。

最後に deps... を添えてできあがり。

~/src# go test repositories/jet_repository_test.go
ok      command-line-arguments  0.030s

うん、うまく動いてるみたいです。

とはいえ依存先レコードのフィールドにも値を入れようとした結果、結構コードが多くなってしまいました。依存先フィールドの指定はやっぱり factoryboy とかのほうが楽ですね〜。 まぁ依存先を指定してないやつ(doveとか)もレコードが作れているので上出来でしょう。

備考

  • 今回は RunTest 関数の中で cases をループさせていましたが、対象関数によってテーブルの内容が変化する場合は cases を外にして、 サブケースごとに RunTest を実行したほうがよいです。

    • 前の結果があとの結果に影響してしまうためです。今回は参照だけなので実行パフォーマンスを考慮しこのような関係にしています。

  • トランザクションのロールバックでDBを初期化するという考えは古代から伝わる良い手法ではあるのですが、 トランザクションやセーブポイントを生でいじるような関数を対象とする場合は NestableTx でも期待通り動作しないおそれがあります。

    • この場合はテーブルを個別に TRUNCATE するとかもありかもしれません。

      • "SET FOREIGN_KEY_CHECKS = 0" とかで外部キー制約のチェックを外す必要がある。

この記事があなたの助けになればうれしいです。 いいなと思ったらシェアお願いしますね。