GoアプリケーションでのRDBを使用した自動テストのすすめ
tmpfs・txdb・t.Parallel()でGoのRDB結合テストを高速化するTips
Table of contents
author: oikawa-k
はじめに
最近はGoアプリケーションの自動テストで、RDBを結合した状態のままテストを書くケースが増えてきました。 自動テストの原理主義に従うとDB接続部分はmockに置き換えるべきですが、それだとアプリケーションの挙動を満足に確認できないことが多く、またハードウェアの性能向上もあってRDBを含めたテストでも現実的な時間に収まるようになってきています。
とはいえ、テスト量が増えてくるとDB部分がボトルネックとなり、テスト実行時間がじわじわと遅くなってきます。 本記事では、GoでRDBを使う自動テストを高速化するためのTipsを紹介します。
大前提: 自動化されたテストは高速であるべき
ケント・ベックが「単体テストは高速で実行されるべき」と訴えていた通り、自動化されたテストは高速に実行されなければなりません。 テスト完了までに時間がかかると、以下のような問題が発生します。
- フィードバックサイクルが遅れる
- 単純に開発効率が下がる(待たされるため)
- CI/CDの効率も低下する(テスト完了しないとmergeできないルールだとかなり顕著)
- 開発者のモチベーションが下がる
「RDBを結合してでも実装を確認したい」というのは現実的な選択ですが、その上で速度をなんとか維持する工夫が必要になります。
最適化1: tmpfsを活用する
ローカルでRDBをdockerで管理している場合、docker-compose.ymlの設定でtmpfsを利用できます。
これによってRDBのデータディレクトリがメモリ上に載るため、ディスクI/Oが削減されて高速化が期待できます。
services:
db:
image: postgres:16
tmpfs:
- /var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password
ただし注意点として、効果がはっきり表れたのはMacOSでの実行時でした。Linuxではファイルのメモリキャッシュ機能が優秀で、tmpfsを使わなくても元々十分に高速だったためです。
結論: Linuxを使おう。
最適化2: txdbを使用する
テストケースが増えてくると、データ初期化や各ケースごとのデータ投入で時間がかかるようになります。 さらに、テストケース同士でのデータ競合を避けるためにテストの並列化が行えなくなる、ということもよくあります。 RDBを使用した自動テストはプロジェクトが肥大化すると本当にとんでもなく時間がかかるようになるので注意が必要です。
そこで活躍するのが go-txdb です。
txdbとは
txdbは、トランザクションベースのSQL DB driverです。 通常のSQL driverと同じインターフェースで使えますが、実態はトランザクションなので、コネクションプールを閉じるときにロールバックされます。
主なメリットは以下の通りです。
- テストケースごとのデータクリーニング処理が不要になる
- アプリケーションでコミット操作しても、実際にはDBへコミットされないのでテストが速い
- 冪等性が担保されるので、ほかのテストと競合してイライラすることがない
使い方のイメージは以下です。
import (
"database/sql"
"testing"
"github.com/DATA-DOG/go-txdb"
_ "github.com/lib/pq"
)
func init() {
txdb.Register("txdb", "postgres", "host=localhost user=postgres dbname=test sslmode=disable")
}
func TestSomething(t *testing.T) {
db, err := sql.Open("txdb", t.Name())
if err != nil {
t.Fatal(err)
}
defer db.Close()
// テスト本体
}
sql.Openの第2引数(コネクション識別子)にユニークな値を渡せば、テストごとに独立したトランザクションが張られます。 テスト終了時にdb.Close() するとトランザクションがロールバックされ、DBの状態が元に戻る、という仕組みです。
なお、txdbを利用するためには、テスト対象のアプリケーションが外部からデータベース接続(*sql.DB)を受け取れるように設計されている(依存性注入:DI)必要があります。アプリケーション内部で独自に接続を開いている場合はtxdbによるロールバックが機能しないため注意してください。
最適化3: t.Parallel()を使用する
Goのテストでは、テスト関数中で t.Parallel() を呼び出すとそのテストは並列実行されます。
func TestFoo(t *testing.T) {
t.Parallel()
// ...
}
単純に並列化してしまうとDBのデータが競合してしまうため、前項で紹介したtxdbなどでテストケース同士のデータが競合しないようにする工夫が必須です。 ただし、並列化してもDBがボトルネックになるケースは多いので劇的に早くなるわけではない、という点は認識しておいたほうがよいでしょう。
まとめ
- 自動テストは高速であるべき
- tmpfsを使って高速化(Linuxを使うのがおすすめ)
- txdbを使って高速化
t.Parallel()を使って高速化
※本記事は、ジーアイクラウド株式会社の見解を述べたものであり、必要な調査・検討は行っているものの必ずしもその正確性や真実性を保証するものではありません。
※リンクを利用する際には、必ず出典がGIC dryaki-blogであることを明記してください。
リンクの利用によりトラブルが発生した場合、リンクを設置した方ご自身の責任で対応してください。
ジーアイクラウド株式会社はユーザーによるリンクの利用につき、如何なる責任を負うものではありません。