レポート用の SQL ビルダーを信頼できるものにするまで
こんにちは。超SSS級の開発者になりたい李昇宰です。本稿は、広告レポートを作る SQL ビルダーを一つ、信頼できるものにするまでの記録です。
私が担当するサービスには、SQL を使ってレポートを作るビルダーがあります。ユーザーが「先月のキャンペーン別のコンバージョン数を見せて」のように自然言語でリクエストすると、AI エージェントがその文を解釈して、ディメンション・指標・期間・フィルターをパラメータとして抽出します。ビルダーはそのパラメータを受け取り、Athena で実行する SQL を句単位で組み立て、実行結果をレポートとして返します。自然言語の一文が SQL になり、そのまま広告主が見る数字になるわけです。
問題は、この種のコードは間違っていても気づきにくいという点です。SQL の文法が壊れるわけではないので、クエリは問題なく回り、表もきれいに埋まります。売上が重複集計されて二倍に膨らんでも誰も気づかず、数字だけが静かに間違っています。
そこで私は、このビルダーを信頼できるものにするテストを考えました。本稿はその考察の記録です。
私が考えるテストは大きく二つに分かれます。ユニットテストと統合テストです。どちらがより重要かと問われれば、私はどちらも重要だと答えます。それぞれ目的が違うからです。
ユニットテストは契約の検証です。「この入力を与えればこの出力を返す」という契約、つまりスペックを確認します。実際のインフラを持ち出さないのでコスト負担が小さく速いのですが、その環境は実際とは異なります。プロダクションではインフラを呼び出し、モジュール同士が協調し、ネットワークを通って外へ出ていきます。しかしユニットテストはそうではありません。しかもユニットテストはたいてい成功するものです。そもそもスペックを確認するために成功ケースと失敗ケースを分け、そうやって mock で作っておくからです。ですからユニットテストの役割は、スペックの検証と、コード修正時に変更の伝播を捕まえることにあります。
一方、統合テストは程度の差はあれ、実際のプロダクション環境にできる限り近づけようとします。実際の動作をテストできますが、その分だけ重く、場合によっては課金が発生することもあります。
実際のコードがプロダクションへ出ていくには統合テストが必ず必要です。そしてこの二つは常にともに開発されてこそ、テストコードが真価を発揮します。
いざテストをしようとすると問題がありました。S3・Athena といった AWS インフラは整っていましたが、肝心のその上でテストに使うデータがありませんでした。
そこで mock データから始めました。小さな偽データを作って入れ、その値を pandas で検算しておいたうえで、同じ入力をビルダーに入れて作られた SQL の結果と突き合わせました。このようにビルダーが出した数字をビルダーで確認するのではなく、独立した計算と照合するやり方をテストオラクルと呼びます。二つの経路は完全に独立しており、セル単位で一マスでもずれれば失敗です。
この段階ではデータの整合性も見ましたが、主に確認したかったのはスペックでした。SQL が壊れないか、ビルダーが条件に応じて分岐を正しくたどるか、テーブルを正しく選ぶか。ビルダーのロジックが出す SQL の形を検証しようとしました。
実データが投入される瞬間にも、新しいテストオラクルを作ればよいだけでした。幸い実データベースでもよく検証してくれましたが、ほどなく別の問題にぶつかります。要件は常に変わり、バグは見つかります。そのたびにコードをリファクタリングし、テストを整備し直す過程はあまりに反復的で退屈だ、ということです。
テストを設計するにあたり、三つを重視しました。第一に、いつ回しても再現されること。第二に、何をなぜ直したかが追跡されること。第三に、この二つを人の手ではなく自動で回すこと。
第一の原則、再現。検証に使ったデータセットを固定しておき、その上でケース全部を毎回また回します。ビルダーのロジックに手を入れるたびに、同じデータに同じケースを投げて、以前正しかったものが今も正しいかを確認します。一種の回帰テストです。検証スクリプトもリポジトリの中に永続化し、ファイル名だけであのときのあのラウンドをそのまま再び回せるようにしました。
第二の原則、追跡。テストを回すたびに検証ドキュメントを残しました。一つは物語です。目標、データの出所、算式、ラウンド別の結果、失敗したときの原因と直した過程。もう一つはテストセットです。これまで回した SQL の全文と各結果、再現方法。どちらも消さずに append だけします。
第三の原則、自動化。ここまでを毎回人が手でやるわけにはいきません。そこでSQL ビルダー専用のテストエージェントとスキルを作りました。エージェントがケースを広げ、スキルが検証手順を毎回同じ順序で踏みます。再現と追跡が、人の意志ではなくツールの既定の動作になります。
テストを書く仕事は、実装よりいつも難しいように思います。よく書かれたテストは、それだけを見ても元のコードを推測でき、それ自体が実行可能なスペックになります。
あえて開発者の境地を分けるなら、意味のあるテストコードをうまく書ける開発者こそ、冒頭で述べた超SSS級の開発者ではないかと思います。
だからこそ最近は、どうテストするか、どう検証するかについて多くのことを考えています。
お読みいただきありがとうございました。