pgTAPでPostgreSQLのユニットテストを実行:デプロイ時のデータベースの「盲点」をなくす

Database tutorial - IT technology blog
Database tutorial - IT technology blog

アプリケーションのテストが100%パスしても、なぜシステムがクラッシュするのか?

Node.jsやJavaのコードがユニットテストで網羅されていると自信を持っていても、本番環境にデプロイした瞬間にシステムが「ダウン」した経験はありませんか?その原因は、ロジックエラーのあるトリガーや、アップグレード後に間違ったデータ型を返す関数であることが多いです。時には、カラムのデータ型を少し変更しただけで、アプリケーション全体が停止してしまうこともあります。

実際、多くの開発者はデータベースを単なる保存用の「ブラックボックス」として扱っています。しかし、パフォーマンス最適化のためにビジネスロジックをDB層に移行する場合、ユニットテストがないことは時限爆弾を設置するようなものです。pgTAPは、その導火線を外すために誕生しました。

pgTAPとは?

pgTAPは、PostgreSQL専用のエクステンションです。TAP(Test Anything Protocol)プロトコルに基づいたテスト関数を提供します。慣れ親しんだSQL言語を使用して、テーブル構造、制約(Constraints)、複雑な処理関数をチェックするスクリプトを記述できます。

最大の利点は? pgTAPはデータベース内で直接実行されることです。テストケースをトランザクションでラップし、最後に ROLLBACK するだけで完了です。ゴミデータはすべて消え、環境は元のクリーンな状態に戻ります。

2分でできるpgTAPのインストール

UbuntuやDebianを使用している場合は、コマンド1つでインストールできます:

sudo apt-get install postgresql-15-pgtap # 使用しているバージョンに合わせて '15' を変更してください

その後、データベース内でエクステンションを有効にします:

CREATE EXTENSION pgtap;

ターミナルでレポートを視覚的に確認するには、pg_prove を追加でインストールしてください。これはテストを一括実行するための非常にプロフェッショナルなツールです:

sudo cpan TAP::Parser::SourceHandler::pgTAP

実践的なテストの実施

以下は、データの保護に頻繁に適用される3つのテストシナリオです。

1. 構造(スキーマ)のテスト

「カラムが存在しません」というエラーに悩まされないようにしましょう。users テーブルに標準形式の email カラムが常に存在することを確認します。

BEGIN;
SELECT plan(3);

-- テーブルとカラムのチェック
SELECT has_table('users', 'usersテーブルが存在すること');
SELECT col_type_is('users', 'email', 'text', 'emailカラムはtext型であること');
SELECT col_is_unique('users', 'email', 'emailカラムはUnique制約を持つこと');

SELECT * FROM finish();
ROLLBACK;

2. 関数ロジックのテスト

例えば、calculate_discount 関数があるとします。手動でテストする代わりに、pgTAPに小数点以下の正確な結果を確認させましょう。

BEGIN;
SELECT plan(2);

-- テストケース:100,000の注文に対して10%割引
SELECT results_eq(
    'SELECT calculate_discount(100000)',
    'SELECT 90000::numeric',
    '入力値が100,000の場合、関数は90,000を返すべき'
);

SELECT * FROM finish();
ROLLBACK;

大規模なプロジェクトでは、CSVから何千行ものサンプルデータを処理することがよくあります。時間を節約するために、toolcraft.app/ja/tools/data/csv-to-json などのツールを使ってJSONBに素早く変換しています。このツールはブラウザ上で完全に動作するため、内部データにとっても非常に安全です。

3. 「隠れた」トリガーのテスト

トリガーは目視だけでデバッグするのは非常に困難です。変更があった際に updated_at カラムが常に自動更新されることを確認するテストを書きましょう。

BEGIN;
SELECT plan(1);

INSERT INTO users (id, username) VALUES (1, 'dev_test');
SELECT pg_sleep(0.1); -- タイムスタンプをずらすために少し待機
UPDATE users SET username = 'dev_updated' WHERE id = 1;

SELECT ok(
    updated_at > created_at,
    'ユーザー更新時にトリガーが時間を自動更新すること'
) FROM users WHERE id = 1;

SELECT * FROM finish();
ROLLBACK;

CI/CDによる自動化

手動で実行する代わりに、すべてのファイルを /tests ディレクトリにまとめましょう。コマンド1つで、システム全体に問題がないかを確認できます:

pg_prove -U postgres -d my_database tests/*.sql

テストケースが1つでも失敗すると、pg_prove は0以外の終了コードを返します。これにより、GitHub ActionsやGitLab CIのパイプラインが即座に停止し、ステージングデータベースを破損させるリスクを防ぐことができます。

実践的なアドバイス

長年PostgreSQLに携わってきた中で、私が常に守っている3つのルールを紹介します:

  • 常にトランザクションを使用する: BEGINROLLBACK のペアは必須です。これにより、テストのたびにデータベースをクリーンに保つことができます。
  • 環境を分離する: 開発中の共有DBでテストを実行しないでください。同僚とのデータ競合を避けるために、専用のインスタンスを作成しましょう。
  • 優先順位をつける: 当たり前のことをテストするのに時間を費やさないでください。数百行に及ぶストアドプロシージャや、複雑に絡み合ったトリガーのテストに集中しましょう。

おわりに

データベースのユニットテストを書くことは、最初は少し時間がかかるかもしれません。しかし、長期的にはデバッグ時間を30〜40%節約できます。pgTAPという守護神がいれば、大規模なシステムのリファクタリングも自信を持って行えるようになります。今日から導入して、その違いを実感してみてください!

Share: