はじめに
Goで実装されているアプリケーションでデータベース接続のヘルスチェックのテストをするさいに検討した内容です。
アドカレのネタを提供してくれた よっしー と あおしょー には感謝です。
データベースへの接続状態をチェックするヘルスチェックのテスト
k8sにデプロイされているアプリケーションからいくつかのレイヤを経由してデータベースに接続できることの確認をするヘルスチェック用APIのエンドポイントがあります。
データベース接続に異常があった場合、エラーを検知しアラートを上げる仕組みとして使われるエンドポイントで、エラー際のテストパターンについて検討しました。
現在の実装ベースで検討
ヘルスチェック処理ではアプリケーションで利用しているデータベース接続のコードを利用しており init()
メソッド で接続情報を設定し起動中は変更できないようになっています。
正常系のテストは sql.Ping()
(実装は select 1;
) で疎通を確認してエラーが nil
であればで成功ですが、エラーのテストをどうするのがいいか悩みました。
テスト内で os.Setenv
で接続情報を変更してみようと試みますが、init()
メソッドで設定しているので変わらないですね。
テスト対象のコードとデータベース接続のコードは別のパッケージにあり、export_test.go
で変更することができません。
テスト自体を分ける
接続情報を設定が切り替えられればどうにかなりそうなので go help testflag
でなにか使えそうなものを探してみます。
-run regexp
Run only those tests, examples, and fuzz tests matching the regular
expression. For tests, the regular expression is split by unbracketed
slash (/) characters into a sequence of regular expressions, and each
part of a test's identifier must match the corresponding element in
the sequence, if any. Note that possible parents of matches are
run too, so that -run=X/Y matches and runs and reports the result
of all tests matching X, even those without sub-tests matching Y,
because it must run them to look for those sub-tests.
See also -skip.
接続情報を変更したいのでテストの実行を分けるというのもできそう。
See also -skip とのことなのでこちらも。
-skip regexp
Run only those tests, examples, fuzz tests, and benchmarks that
do not match the regular expression. Like for -run and -bench,
for tests and benchmarks, the regular expression is split by unbracketed
slash (/) characters into a sequence of regular expressions, and each
part of a test's identifier must match the corresponding element in
the sequence, if any.
設定を変更しないとテストしづらい異常系や準正常系などが複数あって分けるメリットがあれば良さそうですがそこまでではないので別の方法を検討。
データベース接続用の構造体を exported な変数に代入して変更するのも一つのアイデアですが、テストコードに閉じた対応ができないか探ってみます。
テスト実行中のみデータベースに接続できない状態にする
他のデータベース系のヘルスチェックのテストを見てみると mysql でユーザのパスワードを一時的に変更して接続できない状態にしているテストがあったためその仕組みを利用してみます。
今回はSQLServerのヘルスチェック追加のため検証は mcr.microsoft.com/azure-sql-edge
のイメージで行っています。
検証コード
password := "currentPasSword"
dummyPassword := "dummyPasSword"
user := "user"
host := "localhost:1433"
u := &url.URL{
Scheme: "sqlserver",
User: url.UserPassword(user, password),
Host: host,
}
t.Cleanup(func() {
// テスト終了後に変更後のパスワードで接続
u.User = url.UserPassword(user, dummyPassword)
cleanupDb, err := sql.Open("sqlserver", u.String())
if err != nil {
panic(err)
}
defer cleanupDb.Close()
// 元のパスワードに戻す
if _, err = cleanupDb.Exec(fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD = '%s' OLD_PASSWORD = '%s'", user, password, dummyPassword)); err != nil {
panic(err)
}
})
db, err := sql.Open("sqlserver", u.String())
if err != nil {
panic(err)
}
defer db.Close()
// テスト時に接続できない状態にするためパスワードを変更
if _, err = db.Exec(fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD = '%s' OLD_PASSWORD = '%s'", user, dummyPassword, password))
; err != nil {
panic(err)
}
// health check test code...
mysqlとの、違いとして azure-sql-edge ではパスワードの変更後に接続が
Cannot continue the execution because the session is in the kill state.
となり、次のクエリを受け付けない状態となります。
そのためパスワードを戻す際は変更後のパスワードで再接続し、新しいコネクションでパスワードを戻す実装にしてみました。
エラーとなる処理
if _, err = db.Exec(fmt.Sprintf("ALTER LOGIN %s WITH PASSWORD = '%s' OLD_PASSWORD = '%s'", user, dummyPassword, password))
; err != nil {
panic(err)
}
// パスワード変更後はsessionが切れる
if err = db.Ping(); err != nil {
// Cannot continue the execution because the session is in the kill state.
panic(err)
}
接続できないパターンを細かくテストするようなケースもあると思いますが、今回はアプリケーションで利用できない状態が検知できればOKなのでこのようなパターンで良さそうです。
おわりに
「コードを変えずに結果を変えろ」的なミッションで楽しいPRでした。
他にもやり方はあるとおもうので、採用はできないけどこんなやり方がある的な話をチームでしてみても面白かなと思いました。
なすを洗っている時にキュッキュとなるとイルカのくちばしを思い出すそんな2023年でした。