超SSS級の開発者はテストを書く

レポート用の SQL ビルダーを信頼できるものにするまで

李昇宰 · NHN AD
プラットフォームサービスラボ · 2026年7月 · 約8分

はじめに

こんにちは。超SSS級の開発者になりたい李昇宰です。本稿は、広告レポートを作る SQL ビルダーを一つ、信頼できるものにするまでの記録です。

自然言語がレポートになるまで

私が担当するサービスには、SQL を使ってレポートを作るビルダーがあります。ユーザーが「先月のキャンペーン別のコンバージョン数を見せて」のように自然言語でリクエストすると、AI エージェントがその文を解釈して、ディメンション・指標・期間・フィルターをパラメータとして抽出します。ビルダーはそのパラメータを受け取り、Athena で実行する SQL を句単位で組み立て、実行結果をレポートとして返します。自然言語の一文が SQL になり、そのまま広告主が見る数字になるわけです。

自然言語の質問 ディメンション・指標・期間・フィルター AI エージェント パラメータ抽出・正規化 SQL ビルダー 句単位で組み立て クエリを句単位で別々に作って組み立て SELECT GROUP BY WHERE HAVING ORDER BY LIMIT Athena 実行・キャッシュ レポート 数字を返す
自然言語のリクエストが AI エージェントを経て SQL に組み立てられ、Athena で実行されてレポートになる

問題は、この種のコードは間違っていても気づきにくいという点です。SQL の文法が壊れるわけではないので、クエリは問題なく回り、表もきれいに埋まります。売上が重複集計されて二倍に膨らんでも誰も気づかず、数字だけが静かに間違っています。

レポート結果 クエリ成功 ✓ 項目 売上 実際の売上 ₩1,000,000 レポートの売上 ₩2,000,000 重複集計 ×2 SQL の文法は正常 — 値だけが静かに間違う レポートの売上がなぜ 実際と一致しないんだ? 広告主
SQL は成功したのに売上が二倍に — 文法ではなく値が静かに間違う

そこで私は、このビルダーを信頼できるものにするテストを考えました。本稿はその考察の記録です。

テストについての考察

私が考えるテストは大きく二つに分かれます。ユニットテストと統合テストです。どちらがより重要かと問われれば、私はどちらも重要だと答えます。それぞれ目的が違うからです。

ユニットテストは契約の検証です。「この入力を与えればこの出力を返す」という契約、つまりスペックを確認します。実際のインフラを持ち出さないのでコスト負担が小さく速いのですが、その環境は実際とは異なります。プロダクションではインフラを呼び出し、モジュール同士が協調し、ネットワークを通って外へ出ていきます。しかしユニットテストはそうではありません。しかもユニットテストはたいてい成功するものです。そもそもスペックを確認するために成功ケースと失敗ケースを分け、そうやって mock で作っておくからです。ですからユニットテストの役割は、スペックの検証と、コード修正時に変更の伝播を捕まえることにあります。

一方、統合テストは程度の差はあれ、実際のプロダクション環境にできる限り近づけようとします。実際の動作をテストできますが、その分だけ重く、場合によっては課金が発生することもあります。

実際のコードがプロダクションへ出ていくには統合テストが必ず必要です。そしてこの二つは常にともに開発されてこそ、テストコードが真価を発揮します。

ユニットテスト 契約 · スペック検証 · mock で成功・失敗を分離 · インフラなしで安く · 速く回す 変更の伝播を捕まえる 統合テスト プロダクションに近づけて · インフラ呼び出し · モジュール同士の協調 · ネットワークを通って外へ プロダクション出荷の必須条件 二つは常にともに開発されてこそテストが真価を発揮する
ユニットは契約を安く確認し、統合はプロダクションの協調を確認する · 二つはともに進む

データがないのにどうやってテストするの

いざテストをしようとすると問題がありました。S3・Athena といった AWS インフラは整っていましたが、肝心のその上でテストに使うデータがありませんでした。

そこで mock データから始めました。小さな偽データを作って入れ、その値を pandas で検算しておいたうえで、同じ入力をビルダーに入れて作られた SQL の結果と突き合わせました。このようにビルダーが出した数字をビルダーで確認するのではなく、独立した計算と照合するやり方をテストオラクルと呼びます。二つの経路は完全に独立しており、セル単位で一マスでもずれれば失敗です。

ビルダーの SQL 実 Athena 実行 元データ pandas で独立再計算 結果 A 結果 B セル単位で照合 不一致 0 = 通過
正解を知る独立した計算が別にあってこそ検証が成立する · 二つの経路がセル一つまで一致してこそ通過

この段階ではデータの整合性も見ましたが、主に確認したかったのはスペックでした。SQL が壊れないか、ビルダーが条件に応じて分岐を正しくたどるか、テーブルを正しく選ぶか。ビルダーのロジックが出す SQL の形を検証しようとしました。

実データが投入される瞬間にも、新しいテストオラクルを作ればよいだけでした。幸い実データベースでもよく検証してくれましたが、ほどなく別の問題にぶつかります。要件は常に変わり、バグは見つかります。そのたびにコードをリファクタリングし、テストを整備し直す過程はあまりに反復的で退屈だ、ということです。

どうして毎回人がやるの

テストを設計するにあたり、三つを重視しました。第一に、いつ回しても再現されること。第二に、何をなぜ直したかが追跡されること。第三に、この二つを人の手ではなく自動で回すこと。

01 再現 固定データセット 回帰テスト スクリプト永続化 いつ回しても同じ結果 02 追跡 物語ドキュメント テストセットドキュメント append only 何をなぜ直したかが残る 03 自動化 テストエージェント スキル 同じ手順を繰り返す 人の手なしで回る
再現 · 追跡 · 自動化 — テスト設計でつかんだ三つ

第一の原則、再現。検証に使ったデータセットを固定しておき、その上でケース全部を毎回また回します。ビルダーのロジックに手を入れるたびに、同じデータに同じケースを投げて、以前正しかったものが今も正しいかを確認します。一種の回帰テストです。検証スクリプトもリポジトリの中に永続化し、ファイル名だけであのときのあのラウンドをそのまま再び回せるようにしました。

第二の原則、追跡。テストを回すたびに検証ドキュメントを残しました。一つは物語です。目標、データの出所、算式、ラウンド別の結果、失敗したときの原因と直した過程。もう一つはテストセットです。これまで回した SQL の全文と各結果、再現方法。どちらも消さずに append だけします。

検証ドキュメント · 物語 目標 · データの出所 · 算式 ラウンド別の結果と判定 失敗原因 · 自ら直した過程 append only SQL ドキュメント · テストセット 回した SQL の全文 各結果 再現方法 append only R1 R2 R3 R9 ラウンドが積み上がるほどドキュメントもともに積み上がる
物語とテストセットを消さずに積み上げる · ファイル名だけであのときのあのラウンドを再び回せるように

第三の原則、自動化。ここまでを毎回人が手でやるわけにはいきません。そこでSQL ビルダー専用のテストエージェントとスキルを作りました。エージェントがケースを広げ、スキルが検証手順を毎回同じ順序で踏みます。再現と追跡が、人の意志ではなくツールの既定の動作になります。

① オラクル再計算 S3 元データ → pandas 独立再計算 verify_core.py · コード非依存 ② 成果物実行 sql_builder → Athena 実行 結果再利用キャッシュ ③ セル単位で照合 キー 1:1 · 3軸 値 · CASE パース · カラム順序 ④ 回帰スイート QA ごとに専用ラウンドを追加 全スイートを再実行 ⑤ append ドキュメント 検証.md 物語 · SQL.md テストセット · QA トラッカー sql_builder を変えるたびに全スイートを繰り返す · スイートは QA ごとに増える
検証スキルの構造 — 独立したオラクルとビルダー実行をセル単位で照合し、QA ごとに専用ラウンドを足して全体を回帰として再実行し、結果をドキュメントに append する

おわりに

テストを書く仕事は、実装よりいつも難しいように思います。よく書かれたテストは、それだけを見ても元のコードを推測でき、それ自体が実行可能なスペックになります。

あえて開発者の境地を分けるなら、意味のあるテストコードをうまく書ける開発者こそ、冒頭で述べた超SSS級の開発者ではないかと思います。

だからこそ最近は、どうテストするか、どう検証するかについて多くのことを考えています。

お読みいただきありがとうございました。

参考