こんにちは。KOUKIです。
Go言語でアプリケーション開発手法を紹介しています。
前回は、sqlcを使ったCRUDの作成について触れました。
今日は、CRUDのUnit Testについて学びたいと思います。
<目次>
必須モジュールのインストール
次のモジュールをインストールしておきましょう。
1 2 |
go get github.com/lib/pq go get github.com/stretchr/testify |
pqモジュールは、Postgresに接続するときに必要です。
testifyは、assert文を提供してくれるGoのテストツールです。
テストファイルの作成
Unit Testを記述するファイルを用意しましょう。
1 2 |
touch db/sqlc/account_test.go touch db/sqlc/main_test.go |
Go – Unit Test
DBの接続準備
main_test.goには、前処理としてDBの接続処理を記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// main_test.go package db import ( "database/sql" "log" "os" "testing" _ "github.com/lib/pq" ) const ( dbDriver = "postgres" dbSource = "postgresql://postgres:secret@localhost:5432/simplebank?sslmode=disable" ) var testQueries *Queries func TestMain(m *testing.M) { // DBに接続 conn, err := sql.Open(dbDriver, dbSource) if err != nil { log.Fatal("cannot connect to db:", err) } defer conn.Close() // 接続情報を渡すとQueryインスタンスを生成 testQueries = New(conn) os.Exit(m.Run()) } |
「func TestMain(m *testing.M)」はイディオムで、全てのテストケースの前と後に実行するBeforeAll
やAfterAll
を実現したい場合に用います。
「dbSource」は、Postgresコンテナへの接続情報を記載しています。ここにつなぎに行くので、テストするときは「docker-compose up」でPostgresコンテナを立ち上げておく必要があります。
「var testQueries *Queries」には、クエリインスタンスが入ります。つまり、このインスタンスからDBを操作できます。
DBテスト – CreateAccount
CreateAccountのテストを実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// account_test.go package db import ( "context" "testing" "github.com/stretchr/testify/require" ) func TestCreateAccount(t *testing.T) { // Arrange --- arg := CreateAccountParams{ Owner: "selfnote", Balance: 100, Currency: "USD", } // Act --- account, err := testQueries.CreateAccount(context.Background(), arg) // Assert --- // 存在チェック require.NoError(t, err) require.NotEmpty(t, account) // 書き込み情報チェック require.Equal(t, arg.Owner, account.Owner) require.Equal(t, arg.Balance, account.Balance) require.Equal(t, arg.Currency, account.Currency) // 0チェック require.NotZero(t, account.ID) require.NotZero(t, account.CreatedAt) } |
CreateAccountParamsにはPostgresに書き込みたい値を設定して、この情報を引数としてtestQueries.CreateAccountに渡しています。
尚、CreateAccountメソッドにはコンテキストが必要なので、「context.Background()」で空のコンテキストを作成しています。
下記のコマンドで、テストを実行してみましょう。
1 2 3 4 5 6 |
$ go test ./... -v ? golang-with-postgres [no test files] === RUN TestCreateAccount --- PASS: TestCreateAccount (0.01s) PASS ok golang-with-postgres/db/sqlc 0.056s |
OKですね。実際にデータが書き込まれているか確認することもできます。
「http://localhost:9232/」にアクセスして、「accounts」タブをクリックしてください。

データが書き込まれていることがわかりますね。私は、テストを2回実行したので、2レコード文書き込まれてますが。。
ユーティリティメソッド – ランダム文字列
テストデータとして「selfnote」を直接打ち込んでいますが、ランダムな文字列でも良さそうです。
ランダムな文字列を生み出すユーティリティメソッドを作成しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
mkdir util touch util/random.go $ tree . ├── Makefile ├── db │ ├── migration │ │ ├── 000001_init_schema.down.sql │ │ └── 000001_init_schema.up.sql │ ├── query │ │ └── account.sql │ └── sqlc │ ├── account.sql.go │ ├── account_test.go │ ├── db.go │ ├── main_test.go │ └── models.go ├── docker │ ├── golang │ │ └── Dockerfile │ └── postgres │ ├── Dockerfile │ └── Simple\ bank.sql ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go ├── sqlc.yaml └── util <<<< └── random.go <<<< |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package util import ( "math/rand" "strings" "time" ) const alphabet = "abcdefghijklmnopqrstuvwxyz" func init() { rand.Seed(time.Now().UnixNano()) } func RandomInt(min, max int64) int64 { return min + rand.Int63n(max-min+1) } func RandomString(n int) string { var sb strings.Builder k := len(alphabet) for i := 0; i < n; i++ { c := alphabet[rand.Intn(k)] sb.WriteByte(c) } return sb.String() } // Owner用 func RandomOwner() string { return RandomString(6) } // Money用 func RandomMoney() int64 { return RandomInt(0, 1000) } // Currency用 func RandomCurrency() string { currencies := []string{"EUR", "USD", "CAD"} n := len(currencies) return currencies[rand.Intn(n)] } |
「rand.Seed(time.Now().UnixNano())」は、同じ乱数が取得されてしまうことを防ぐための設定です。
乱数を与えるためにはSeedを設定する必要がありますが、math/randに「var globalRand = New(&lockedSource{src: NewSource(1).(Source64)})」を見ると、Seedに「NewSource(1) 」が固定されており、プログラム実行毎に同じSeedが渡されています。そのため、取得される乱数が全て同じになるようです。
代わりに現在時刻をSeedとして渡すことで、異なる乱数を得ることができます。
このコードをテストコードに追加します。
1 2 3 4 5 6 |
// Arrange --- arg := CreateAccountParams{ Owner: util.RandomOwner(), Balance: util.RandomMoney(), Currency: util.RandomCurrency(), } |
テストを実行してみましょう。
1 |
go test ./... -v |

ランダムの値が入るようになりましたね。これは便利です。
Makefileにテストコマンドを追加追加
テストコマンドをMakefileに追加しておきましょう。
1 2 3 4 5 |
# Unit Test test: go test -v -cover ./... .PHONY: up createdb dropdb migrateup migratedown createsc sqlc test |
1 2 3 4 5 6 7 8 9 |
$ make test go test -v -cover ./... ? golang-with-postgres [no test files] === RUN TestCreateAccount --- PASS: TestCreateAccount (0.01s) PASS coverage: 16.1% of statements ok golang-with-postgres/db/sqlc 0.052s coverage: 16.1% of statements ? golang-with-postgres/util [no test files] |
ユーティリティメソッド – ランダムアカウント作成
ランダム文字列を生成できるようになったので、次にランダムアカウントを作成する関数を定義しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// account_test.go ... func createRandomAccount(t *testing.T) Account { // Arrange --- arg := CreateAccountParams{ Owner: util.RandomOwner(), Balance: util.RandomMoney(), Currency: util.RandomCurrency(), } // Act --- account, err := testQueries.CreateAccount(context.Background(), arg) // Assert --- // 存在チェック require.NoError(t, err) require.NotEmpty(t, account) // 書き込み情報チェック require.Equal(t, arg.Owner, account.Owner) require.Equal(t, arg.Balance, account.Balance) require.Equal(t, arg.Currency, account.Currency) // 0チェック require.NotZero(t, account.ID) require.NotZero(t, account.CreatedAt) return account } func TestCreateAccount(t *testing.T) { createRandomAccount(t) } |
DBテスト – GetAccount
続いて、GetAccountのテストコードを実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func TestGetAccount(t *testing.T) { // Arrange --- account1 := createRandomAccount(t) // Act --- account2, err := testQueries.GetAccount(context.Background(), account1.ID) // Assert --- require.NoError(t, err) require.NotEmpty(t, account2) require.Equal(t, account1.ID, account2.ID) require.Equal(t, account1.Owner, account2.Owner) require.Equal(t, account1.Balance, account2.Balance) require.Equal(t, account1.Currency, account2.Currency) require.WithinDuration(t, account1.CreatedAt, account2.CreatedAt, time.Second) } |
GetAccountは、ユーザー情報を取得するメソッドなので、createRandomAccount関数にて作成したユーザーの情報が取得できるかテストしています。
1 2 3 4 5 6 7 8 9 10 11 |
$ make test go test -v -cover ./... ? golang-with-postgres [no test files] === RUN TestCreateAccount --- PASS: TestCreateAccount (0.01s) === RUN TestGetAccount --- PASS: TestGetAccount (0.00s) PASS coverage: 29.0% of statements ok golang-with-postgres/db/sqlc 0.027s coverage: 29.0% of statements ? golang-with-postgres/util [no test files] |
問題なさそうですね。
DBテスト – UpdateAccount
続いて、UpdateAccountのテストを実装しましょう。このメソッドは、Updateなので、createRandomAccount関数で作成したユーザーのBalanceを更新しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func TestUpdateAccount(t *testing.T) { // Arrange --- account1 := createRandomAccount(t) arg := UpdateAccountParams{ ID: account1.ID, Balance: util.RandomMoney(), } // Act --- account2, err := testQueries.UpdateAccount(context.Background(), arg) // Assert --- require.NoError(t, err) require.NotEmpty(t, account2) require.Equal(t, account1.ID, account2.ID) require.Equal(t, account1.Owner, account2.Owner) require.Equal(t, arg.Balance, account2.Balance) require.Equal(t, account1.Currency, account2.Currency) require.WithinDuration(t, account1.CreatedAt, account2.CreatedAt, time.Second) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ make test go test -v -cover ./... ? golang-with-postgres [no test files] === RUN TestCreateAccount --- PASS: TestCreateAccount (0.01s) === RUN TestGetAccount --- PASS: TestGetAccount (0.00s) === RUN TestUpdateAccount --- PASS: TestUpdateAccount (0.01s) PASS coverage: 41.9% of statements ok golang-with-postgres/db/sqlc 0.037s coverage: 41.9% of statements ? golang-with-postgres/util [no test files] |
DBテスト – DeleteAccount
続いて、DeleteAccountをテストします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func TestDeleteAccount(t *testing.T) { // Arrange --- account1 := createRandomAccount(t) // Act --- err := testQueries.DeleteAccount(context.Background(), account1.ID) // Assert --- require.NoError(t, err) account2, err := testQueries.GetAccount(context.Background(), account1.ID) require.Error(t, err) require.EqualError(t, err, sql.ErrNoRows.Error()) require.Empty(t, account2) } |
テストを実行します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ make test go test -v -cover ./... ? golang-with-postgres [no test files] === RUN TestCreateAccount --- PASS: TestCreateAccount (0.02s) === RUN TestGetAccount --- PASS: TestGetAccount (0.01s) === RUN TestUpdateAccount --- PASS: TestUpdateAccount (0.02s) === RUN TestDeleteAccount --- PASS: TestDeleteAccount (0.02s) PASS coverage: 48.4% of statements ok golang-with-postgres/db/sqlc 0.086s coverage: 48.4% of statements ? golang-with-postgres/util [no test files] |
OKですね。
DBテスト – ListAccount
最後に、ListAccountをテストします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func TestListAccounts(t *testing.T) { // Arrange --- for i := 0; i < 10; i++ { createRandomAccount(t) } arg := ListAccountsParams{ Limit: 5, Offset: 5, } // Act --- accounts, err := testQueries.ListAccounts(context.Background(), arg) // Assert --- require.NoError(t, err) require.Len(t, accounts, 5) for _, account := range accounts { require.NotEmpty(t, account) } } |
テストを実行しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ make test go test -v -cover ./... ? golang-with-postgres [no test files] === RUN TestCreateAccount --- PASS: TestCreateAccount (0.01s) === RUN TestGetAccount --- PASS: TestGetAccount (0.01s) === RUN TestUpdateAccount --- PASS: TestUpdateAccount (0.00s) === RUN TestDeleteAccount --- PASS: TestDeleteAccount (0.01s) === RUN TestListAccounts --- PASS: TestListAccounts (0.02s) PASS coverage: 83.9% of statements ok golang-with-postgres/db/sqlc 0.063s coverage: 83.9% of statements ? golang-with-postgres/util [no test files] |
テストがPassしました。少し長くなりましたが、以上です。
次回
テストコードが書ければ、アプリケーション開発のレベルもグッと上がるはずです。色々なパターンを書いてレベル上げていきましょう。
次回は、DBのTransactionについて学びましょう。
コメントを残す
コメントを投稿するにはログインしてください。