Go初心者向けに始めた記事の第7回になります。
今回は、Go言語でのテストコードの書き方について説明します。
テストコードの問題として、他のテストのデータの更新の影響を受けたり、テストの実行順序によって成功したり失敗したりすることがあります。今回は、他のテストに影響を与えないようにするための独立したテストコードの書き方についても紹介します。
他のテストコードに影響を与えないテストコードの書き方
結論として、他のテストに影響を与えないテストコードを作成する方法は、テストコードの前後でデータベースの状態を変更しないようにすることです。具体的な実装方法は、テストをトランザクション内で実行し、各テストケースの終了時にはロールバックを行います。
Goのテストの特徴
- Goのテストコードのファイル名は「対象のテストコードファイル_test」にする。
- テストメソッド名は「TestXXX」にする。
- TDT(Table Driven Test)と呼ばれる形式で実装されていて、入力値と期待値などをまとめて定義して、テストの実行箇所を1箇所にまとめている。そうすることで新しくテストケースを追加する時、容易になります。
実装
インストール
テスト結果の比較に使用するgo-cmpをインストール
go get -u github.com/google/go-cmp/cmp
コード
db/mysql.go
テスト用の接続を追加した。
InTxメソッドで、テストを実行する無記名関数が渡され、テストをトランザクション内で実行させ、最後にロールバックをさせている。これにより常にDBの状態がテスト前後で変わらない。
package db
import (
"context"
"database/sql"
"fmt"
"github.com/SND1231/go-column/setting"
)
func GetDBconnection(dbSetting setting.DB) (*sql.DB, error) {
var dataSource string = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true",
dbSetting.User,
dbSetting.Password,
dbSetting.Host,
dbSetting.Port,
dbSetting.Name,
)
dataSource = dataSource + "&loc=Asia%2FTokyo"
db, err := sql.Open(dbSetting.Type, dataSource)
return db, err
}
func NewUnitTestDB() *UnitTestDB {
conn, err := GetDBconnectionForTest()
if err != nil {
panic(err)
}
return &UnitTestDB{
conn: conn,
}
}
func GetDBconnectionForTest() (*sql.DB, error) {
dbSetting := setting.DB{
Type: "mysql",
User: "root",
Password: "test",
Host: "mysql",
Port: 3306,
Name: "unit_test",
}
return GetDBconnection(dbSetting)
}
type UnitTestDB struct {
conn *sql.DB
}
// 他のテストに影響がないように、テスト終了後にロールバックをしてテスト前と同じ状態にする。
func (db *UnitTestDB) InTx(exec func(context.Context, *sql.Tx)) {
if db.conn == nil {
panic("connection is nil")
}
ctx := context.Background()
tx, err := db.conn.BeginTx(ctx, &sql.TxOptions{})
if err != nil {
panic(err)
}
// ロールバックしてテスト中のデータ作成や更新は他のテストに影響ないようにしている。
defer tx.Rollback()
exec(ctx, tx)
}
usecase/user_test.go
usecase/user.goのテストコード。
casesで入力値と期待値などをまとめて定義している。
testDB.InTx()の中が実際にテストしている無記名関数。
package usecase
import (
"context"
"database/sql"
"testing"
"github.com/SND1231/go-column/db"
"github.com/SND1231/go-column/models"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
_ "github.com/go-sql-driver/mysql"
)
func TestAdd(t *testing.T) {
// テスト用のDB接続取得
testDB := db.NewUnitTestDB()
// Inputの情報で作成した後に、作成されたユーザーIDを元にユーザーを取得して、Wantと比較する。
cases := []struct {
label string
Input AddUserInput
Want *models.User
}{
{
"ユーザー登録のテスト",
AddUserInput{
Name: "John",
Age: 40,
},
&models.User{
Name: "John",
Age: 40,
},
},
}
for _, c := range cases {
t.Run(c.label, func(t *testing.T) {
testDB.InTx(func(ctx context.Context, tx *sql.Tx) {
usecase := NewUserUsecase(ctx, tx)
output, err := usecase.Add(c.Input)
if err != nil {
t.Fatal("error:", err)
}
user, _ := models.FindUser(ctx, tx, output.ID)
// IDは何が入るかわからないので、比較する絡むから外す。
opts := []cmp.Option{
cmpopts.IgnoreFields(models.User{}, "ID"),
}
// 作成したユーザーと、求める結果の比較をしている。
// 差があった場合にエラーにしている。
if diff := cmp.Diff(user, c.Want, opts...); diff != "" {
t.Errorf(diff)
}
})
})
}
}
func TestGet(t *testing.T) {
// テスト用のDB接続取得
testDB := db.NewUnitTestDB()
// Getした結果とWantの情報を比較する
cases := []struct {
label string
Input GetUserInput
Want GetUserOutput
}{
{
"ユーザー情報取得のテスト",
GetUserInput{
ID: 4,
},
GetUserOutput{
ID: 4,
Name: "taro",
Age: 25,
},
},
}
for _, c := range cases {
t.Run(c.label, func(t *testing.T) {
testDB.InTx(func(ctx context.Context, tx *sql.Tx) {
usecase := NewUserUsecase(ctx, tx)
output, err := usecase.Get(c.Input)
if err != nil {
t.Fatal("error:", err)
}
// 取得したユーザーと、求める結果の比較をしている。
// 差があった場合にエラーにしている。
if diff := cmp.Diff(output, c.Want); diff != "" {
t.Errorf(diff)
}
})
})
}
}
schema/test.sql
テスト用に追加した。
--- テスト用のデータベース作成
CREATE DATABASE unit_test;
--- テストデータ
insert into user (name, age) values
('taro', 25),
('jiro', 23);
Makefile
テストコード実行を追加した。
.PHONY: build
build:
go build .
.PHONY: sqlboiler
sqlboiler:
sqlboiler mysql
.PHONY: test
test:
# 毎回テスト実行時にキャッシュを削除
go clean -testcache
# shuffle=onでランダムに実行され、coverでテストのカバレッジを測定している。
go test -shuffle=on -cover ./usecase
最終的に以下のリポジトリになる。
実行
テスト前の準備
テストの準備として、以下を実施
- test.sqlのテスト用のデータベース作成を実行
- init.sqlを実行
- test.sqlのテストデータを実行
テスト実施
「make test」を実行すると以下のようにテスト結果が出力される。
root@09b67536b671:/work# make test
go clean -testcache
go test -shuffle=on -cover ./usecase
ok github.com/SND1231/go-column/usecase 0.015s coverage: 71.4% of statements
まとめ
ここまで7回に渡ってGo開発者として必要な基礎知識は一通り解説してきました。
今後必須だと思った知識が分かり次第、「Go開発者への道」の続きを書いていこうと思います。
「Go開発者への道」以外にも、Goのselectや、mockを使用する方法の記事について書いてますので興味がありましたら読んでいただけたらと思います。
【おすすめ記事のリンク】
コメント