- 真野隼記
- 約 2,300 文字
- 600 View
TIG 真野です。
Goで時刻モックライブラリである、Songmuさん開発のflextimeにはずっとお世話になっています。
素晴らしいライブラリですが、SQLを用いて結果を永続化するようなテストでの利用する時に冗長性を感じました。
例えばあるWeb APIやバッチ処理のテストとして、DB上のデータが想定通りに登録/更新されていることを調べたいときです。検証対象のカラムが例えば created_at 、updated_at だとします。通常は現在日時を登録する項目で、flextime で扱うのにうってつけです。ただし、これを固定化するには、Go側の flextime の値を外からSQLプレスホルダーで渡す必要があります。本来であれば、PostgreSQLであればcurrent_timestamp の関数で済むところを一々外から渡すのは面倒に感じます(仕方ないですが、これがなくなればGoもSQLのコードもスッキリするのにと思うこともしばしば)。
package example |
WITH update_cnt AS ( |
もちろん、created_at, updated_at などの項目を検証から除外すれば上記は気にしなくても良いですが、経験的には検証を外せば外すほど、そのテストの信頼性は落ちるのであまりしたくないです(しばしば、実は更新されていないことが後続フェーズで発覚して苦労します)。この辺はmpywさんの書いている記事のように、PostgreSQLのトリガーなどでカバーするチームも多いかと思いますが、いったんそのやり方は忘れるとします。
func TestUpdateAlreadyRead(t *testing.T) { |
当然、PostgreSQLのもとから用意されている組み込み関数には、flextimeのような時刻固定の仕組みはないです。
この例だと更新系なので1項目ですが、登録だとcreated_at分も合わせて2項目になります。また、SQLで抽出したGo側でゴリゴリ業務ロジックで組み立てて、またDBに書き戻すようなコードを書いていると、ここで書いた now を一々引き回す必要があり面倒です(引き回さないと、微妙に呼び出しタイミングで created_at, updated_at の値が変わって扱いにくくなります)。
PostgreSQL関数 を作成して代用してみる
次のようなテーブルと関数を作成してみる提案です。次の flex_time テーブルと、 flex_timestamp() 関数を定義します。
-- テーブル定義 |
上記を作っておいて、、 flex_timestamp() を呼ぶと、何もしないと現在時刻を返します。
postgres=# SELECT flex_timestamp(); |
テストで使用したい時間を登録します。
postgres=# INSERT INTO flex_time(fix_time) VALUES (TO_TIMESTAMP('2022-04-01 15:30:00', 'YYYY-MM-DD HH24:MI:SS')); |
そうすると時間が固定化されます。
postgres=# SELECT flex_timestamp(); |
当たり前ですが、flex_time テーブルのレコードを削除すれば、現在時刻を返します。
postgres=# TRUNCATE flex_time; |
これを用いれば、current_timestamp 関数とほぼ同等の使い方でSQLを書け、呼び出し元のコードもプレスホルダー文ちょっとすっきりすると思います。
使い終わったら TRUNCATE でキレイにしておくのがお作法になると思います。
利用イメージ
ここで最初の実装例に戻って適用してみます。
func UpdateAlreadyRead(tx *sqlx.Tx, userID string) (int, error) { |
WITH update_cnt AS ( |
テストでは、次のようにテスト時間を固定化したいタイミングで flex_time に登録します。終わったらTRUNCATEはチームでお約束を決めればよいと思います(通常は不要な気がしますが、一応消す例で書いてみました)。
func TestUpdateAlreadyRead(t *testing.T) { |
ちょっとした違いですが、開発・レビューなどの観点で見落としになり得るポイントを1つでも減らせるのが大きいかなと思っています。
Appendix
あまりないかもしれませんが、もしテストを並列に実行しかつ、固定化したい時間を変えたい場合は、コネクション単位で application_name を変え、その単位で設定するように関数を改修しても良いかもしれません(flex_timeだけ分ける意味があるかはさておき)
application_name というカラムを追加したバージョンです。
CREATE TABLE flex_time |
これを用いると、グローバル設定と、アプリケーション固有のテスト時間で区別できます。どちらも設定されるとアプリケーション固有を優先です。
-- グローバル設定 |
application_name ですが、次のようにコネクション接続時に指定できます(参考)
postgresql://user@localhost:5432/postgres?connect_timeout=10&application_name=myapp |
ただ、こういった多段の設定はデータ削除が難しい(気軽にflex_time テーブルを TRUNCATE しにくくなる)ため、やるならapplication_nameは NOT NULL にした運用にしたほうが良いかもしれません。要件に応じて調整ですが、できる限り最初の実装のシンプルモデルの利用に留めるという、用法用量が良い塩梅かと感じます。
さいごに
PostgreSQLに、何も指定がなければ current_timestampを、何か設定されていればその値を返すflex_timestamp関数 を定義して、使ってはどうかという記事でした。
これを思いついたのはちょうどあるプロジェクトの開発ラッシュ終盤で、同僚の辻さんに「こんなん思いついたんですけど~」って声をかけたら、もう開発も終わりですから..と諭されたため導入に失敗しました。そのため、まだ本番稼働&運用実績がゼロのアイデア状態です。機会があればこの仕組を使ってみたいと思います。先駆けてトライしてくださる方も大歓迎です。ぜひ感想をTwitterなどで教えてください。
この記事で公開したサンプルコードは以下にアップしておきました。