テストを書きたいプログラムがSQLの固まりだ

(2009.3.5 テストデータについてちょっと追記)

Working Effectively with Legacy Codeを読んでいます。前半、テストの意義とか概念の紹介(test harness, seamなど)はすばらしい。後半の、個別の状況への対策も、整理されていてありがたいです。自分でも使っているおなじみの手法もあれば、目ウロコなこともある。

でも、こういう↓章があったらよかったなあ。

「テストを書きたいけれどプログラムが巨大なSQLの固まりだ」

いま仕事をしている保守プロジェクトが、そういう状況なわけです。もうすこし整理すると、

  • テーブル数が多く(100以上)、重複した項目も多い(非正規化)
  • ひとつの処理をするのにだいたい、最低5つ以上(10を超えるものも多い)のテーブルを扱っている(参照ならJOINやUNION、更新ならそれぞれにUPDATE/INSERT)
  • 処理を走らせるには5個以上のテーブルにデータが必要
  • そうした参照処理を巨大な(3ページ分くらいの)SQL一発で処理している。
  • テスト用のデータがない。本番データをコピーしてきて、目的に適ったデータを探し出して、そのデータを使ってテストする(当然データは壊れる)。

まずは、テストしたい内容がSQL文にモリっと盛り込まれているのが困る。絞り込み条件やJOIN条件が絡んでくるので、細かいテストがやりにくいわけです。また、表示のためのフォーマットや文字列の結合・編集などもSQLに埋め込まれています。必要なパラメータを与えて、結果をドンと取ってきて、こまごましたassertを入れていくことになります。

そうすると今度は、テストデータがないのが困りものです。1つのテストをするにもいくつかのテーブルに、矛盾がないようデータを作ってやらないといけない。長い期間保守が続いているプロジェクトなため、データベース上のデータは「あるものが正しい」という、データベースのきちんとした意味的定義がわからない状態です。テスト用にデータを作るのも一苦労だし、手で作ったものが妥当なデータなのか、検証できなくなっています。テスト用にはちょっとずつ異なる複数の列が必要になるので、なお面倒になります。

そしてもうひとつ問題になるのは、実行速度です。テストでデータベースをアクセスすると、どうしても時間がかかります。テストデータの準備にも時間がかかるし、OSにも負荷がかかるので、テスト実行時間が間延びしやすくなります。あとでチューニングして時間を短縮することも困難です。

すべてをスッキリ解決する方法があれば、それをMichael C. Feathersが書いてくれていれば素敵なんですが、まあそうはなっていない。実際、簡単に解決できる問題ならば、たぶんもう解決しているわけで、時間をかけてちょっとずつ、事態を改善させることしかできないんだろうなあと考えています。

テストのインフラを整える

ここでインフラと言っているのは、以下の要素です。

  • テストを書くためのフレームワーク
    • xUnitなど
  • テストデータをデータベースに準備する仕組み
    • SetUp/TearDownで呼び出せるもの
    • データをあとでメンテできる必要もある
  • テスト実行環境
    • CIツール(Hudson)などがあるとよい

プロジェクト自体にユニットテスティングの導入ができていないので、そこから話が始まります。特にテストデータを、テストケースの中で単純なSQLAPIで用意するのはしんどいので、データの準備方法、ロード方法、元に戻す方法を枠組みとして準備する必要があります。

システムのアーキテクチャ、プロジェクトやチームの文化にもよりますが、たとえばこんな方法が導入できると考えられます。

  • マスターデータはDBMS上に保持しておく(これは変更しない)
  • 個別のテストに必要なデータhExcelで用意する(1シート1テーブルで、複数テーブルにロードできるように)
  • テストケースからは指定したテストデータをExcelからDBMSにロードして、テストを実行する
  • テスト後はすべてロールバックする(ムリなら、DBMSのバックアップなどを使って元に戻す)

巨大SQLをまるっとテストする

巨大SQLは、テストする手間はかかるものの、入力と出力ははっきりしています。ある特定のデータに、あるパラメータを与えると、結果セットが取得できる。あとは、SQLが正しく機能しているかを確認できるだけのパラメータの組み合わせを投入して、結果を期待値と比較すればよい。

現実にすべての結果をすべて比較するのはシンドイので、以下のような方針でテストを書きます。

  • 必要な項目が取得できているか、1レコード取得して確認する
  • 編集しているフィールド(SUMやCOUNT, 日付や時刻や金額、文字列編集、固定文字への変換など)について、1つずつ確認する
  • 絞り込み条件やJOIN条件をカバーするパラメータのすべての組み合わせで、結果を取得する。期待するレコードが取得できるか、レコードのID(プライマリーキー)で確認する

この時点では、テストデータは苦労して用意するしかないと思います。本番から取ってくるとか、システムの機能を使って作り出すか、手でデータをこしらえるか。

モデル層がきれいにAPI化されていれば、テストデータ作成ツールを作ってもいいでしょう(が、こうしたソフトウェアのプロジェクトで前提が満たされているわけもないですね。

ただし、いったん利用したテストデータは大事に取っておく。DBMSのバックアップなり、CSVExcelにエクスポートするなり、INSERT/UPDATE文に落としておくなりして、再利用できる形にしておきます。これは、今回の機能のテストだけでなく、他の箇所のテストでも再利用できる可能性があります。テストに使えるデータという資産として、蓄積できます。(資産なので、データスキーマの変更などに耐えられる形にしておきたい。その点、DBMSのバックアップ形式であれば、スキーマ変更・データ移行のDDLを流してあげれば追随しやすいですね。)

SQLを文字列としてテストする

SQLは、文字列編集で組み立てる場合があります(検索条件が任意の組み合わせが使える場合など)。こうしたSQLは、実行しての結果も必要ですが、文字列編集が正しいかどうかをテストすることもできます。

巨大SQLを分割する

入力に対する出力がまるっとテストできるようになったら、SQLの分割を試みます。これはSQLリファクタリング的なものかもしれませんが(そういうテクニックってあるのかな)たとえば、

  • UNIONしているものであれば、2回のSELECTに簡単に分割できる。
  • まず1回のSELECTでキーだけ取得して、2回目のSELECTで必要な項目を取得する。
  • マスターなどの取得を別SQLにする。メモリにキャッシュすることも可能なら検討してよい。
  • 編集フィールドは、SQLから外してアプリケーション側のロジックで記述する。(どっちみちそのほうが楽な場合も多い。)
  • VIEWを作る

このような変更を加えても、テストがあればカバーしてくれます。また変更に従って、テストを分割できるようになるはずです。SQLが2つになれば、テストも2つにわけ、それぞれをテストできるようになります。これでテストはよりシンプルになる。

データベース操作を隠蔽する

SQLを分割すると、1テーブル、ないし1テーブル+2,3の関連テーブルへの単純なSELECT文にできるかもしれません。あるていどシンプルになったSQLは、いくつかの処理で共有できるロジックとなるかもしれません。

こういったSQLは独立したオブジェクトにメソッドとして持たせられます。そうすると、コード上ではデータベース操作によりわかりやすい名前をつけ、分類できます。さらにテスト上では、このオブジェクトのモックを使えば、データベースを使わなくてもテストができるようになります。

データベース操作をオブジェクトに隠蔽して名前をつけていくと、データベースよりオブジェクトを意識した設計になっていきます。そうなると、アプリケーション全体をデータベースのスキーマが規定している状態から、スキーマは実装の詳細という状態に変化します。アーキテクチャが変わるわけです。そうなれば、データベーススキーマの変更(正規化など)も、影響範囲を限定させた状態でおこなえるようになります。

まとめ

というふうにうまくいったらいいなあと妄想している←イマココ のですが、どうでしょうね。なんにしても、骨が折れる仕事のことは変わりないので、もっと楽に進めるやり方があればいいのになあ。