2006-02-12

最近読んだ本

海辺のカフカ

もらいもの. すばらしく面白かった. (実はもう内容をほとんど覚えていないが, それは良い娯楽作品の条件だと思う...) 村上春樹はなんというか, 日本語が卓越しているね. こんなにストレスなく読めて, かつ下品でない日本語を書ける作家は他に見ない.

アマゾン・ドット・コムの光と影

前から気になっていたのを近所の本屋で発見し読む. 資本主義的に良い会社で働くのが幸せとは限らないのは実感としては 当り前ではあるが, Amazon(JP) の配送センターで働くという極端なケースの体験記. Amazon は素晴しい企業だ. 流通やアルバイトの活用といった計算機的でない部分の出来が良いと 計算機は威力を発揮するのだなあ.

Expert One-on-One J2EE Development without EJB

Spring Framwork の思想に基いた J2EE のアプリケーション開発の指南書. とても勉強になった. 前作も良い本らしい(読んでない)が, この本では各構成要素が Spring の使い方の実例を交え説明されており 実践的な内容になっている. 世には色々 DI コンテナの実装があるが, この本があるという事実は Spring を使う気にさせる.

読み物としての面白さもある. 作者の Rod Johnson はなかなか饒舌かつやや軽薄な人らしい ものすごい勢いで繰り返し EJB を批判する様が微笑ましくていい. 時々不用意なことを言ったりもして, そういうキャラクタで TheServerSide.com を盛り上げているのかなーなどと想像した.

饒舌さには欠点もある. 読み物の部分を割り引いても記述は冗長. 冒頭で "This is a fairly short book" と書いておきながら 500 ページ以上あるのはやや無理があると思う. EJB の悪口と オープンソースの伝道と Spring の優位を熱弁する部分を削れば 2/3 くらいにはなりそう. とはいえこの熱意が Spring を支えているようにも思えるから, そこは御愛嬌ということで読めばいいんだろうね.

テストで手を抜く

私はふだん割と自動テストや単体テストに依存した開発をしている. だから単体テストの素晴しさを説く Rod Johnson には強く同意する. ただ, いきなりこうやれと言われると敷居を高く感じる人も多いだろう. もう少し手抜きをして楽に自動テストをする方法をいくつか紹介しておく. 以下に示す方法の自動テストは完全な単体テストより品質は劣るものの, 開発者レベルのテストが何もない状態よりはだいぶましになる. こうした手抜きテストが定着したら少しずつまっとうな方法に移行していけばいい. 不可能な完璧よりより良い不完全を目指そう.

予定調和テスト : 試行結果を期待動作にする

"レッド・グリーン・リファクター" がテスト駆動の基本だが, レッドになるための assert() 条件をつくるのは面倒なことが多い. たとえばデータを XML で保存する機能をテストでは, インデントなどのクセを XML シリアライザにあわせた 正解データを書かないといけない. また仕様が曖昧でトライアンドエラーをしている時は 何が正しいのか自分でもわかっていない.

そういう時はまず assert() なしで, テスト対象の API を呼ぶだけのテストを書こう. 次にテストを動かしながらテストしたいコード, API の中身を書く. トライアンドエラーをして, なんとなく正しく動いていそうなところまで出来上がったら テストの最後にブレークポイントを貼ってオブジェクトを覗き, その状態が true になるような assert() を書こう. ファイルを出力するテストなら, それを適当なファイルにダンプして期待結果のデータにする.

こうしたリバース・エンジニアリングのような テストはパスして当たり前だから, その意義を疑う人がいるかもしれない. しかしテストはもっぱら後の修正やリファクタリングでトラブルを防ぐときに役立つ ものだから, そうした目的にはこの予定調和テストも十分に役目を果す.

抜き打ちテスト : 検証を部分一致で済ませる

ファイルやバイト列を出力する機能のテストでは, 期待される結果と同じものをテストデータとして用意しておき 結果を memcpy() などで比較するのが素朴な方法だろう. この方法は悪くないが, 大抵はデータを作るのが面倒だ. (前述の予定調和テストと似た問題.) そういう場合は, データの特徴的な部分だけをチェックしよう. 新しく実装したのがデータに項目を追加する機能なら, その項目があることだけをチェックしよう. このアプローチは厳密さを損うが, 大抵は十分だ. またテストが仕様や実装の変更に強くなるという望ましい副作用もある.

チェックには, テストデータが XML なら XPath, 単なるテキストデータなら正規表現を 使うと簡単でいい. Ruby on Rails のテストユーティリティにも XPath による検証機能が含まれている. GUI 中心の対話的なソフトウェア, たとえば 3D エンジンなどを作っていて画像が出力結果という場合は, 適当サンプルしたいくつかのピクセルの色を 期待色と比較するくらいでまずは十分だろう. ハードウェアの差異を吸収する画像比較アルゴリズムなどと言いだすと 収集がつかなくなる.

クラッシュ・グリーン・リファクター : テストで障害を再現する

エラーケースのテストは面倒だ. パターンを網羅するのも面倒だし, エラーの起こる条件を再現するのも面倒. サード・パーティのライブラリが例外を宣言しているから キャッチ節を書いてみたものの, どうすればその例外が起こるのかわからないことはよくある. (C のライブラリなら API の戻り値チェックについて同様のことがいえる.)

そういう箇所はとりあえず放っておこう. 開発を進めていくと, いずれそういう未テストのエラーケースが関係した障害に出くわすだろう. 障害が報告され, 調査の末に原因のあたりがついたら, さっそく修正したい誘惑をこらえてまずはその障害を再現するテストを書こう. その障害がクラッシュするというものなら, テストもクラッシュすれば準備完了だ. 準備ができたらテストが通るようコードを修正しよう.

この方法の利点は, 網羅的なテストをしなくても 障害の出やすいきわどい部分のテストが重点的に行えることだ. 一般にコードの中で障害の発生する箇所は偏っている. 実際, 障害の多くは以前修正したもののデグレードだ. このテストはそのデグレードを防ぐことができる. デグレードはバグの中でも特に(顧客や上司, QA エンジニアからの)評判が悪いため, デグレードを防ぐ価値は大きい.

あたりまえのことだが, 障害の修正後も障害再現テストは消さずにチェックインすること. 蓄積されてこそデグレードを防ぐことができる. 機能追加の最中に昔書いたこれらテストが失敗した時の, 冷や汗と安堵を想像してほしい.

Test over Implementation : インターフェイスを省略する

TDD では Programming over Interface を強調する. クラス間の関連を(言語機能としての)インターフェイスで分離し, 各クラスの単体テストには関連するインターフェイスのモックやスタブを使う. これは極めてまっとうな主張で, できるものならそうしたいがいかんせん面倒だ. クラスを一つ実装するたびに 常に対応するインターフェイスを用意する必要がある. リファクタリング機能があればさほど大変ではないものの, IDE の弱い C++ などを使っているとインターフェイスの同期は手間がかかる.

実際のところ, インターフェイスを用意しなくてもけっこうテストはできる. たとえば A <- B <- C という依存関係にある クラス A, B, C があるとして, 自分が A, B, C すべての実装を担当する場合を考える. この場合は A, B, C の順に実装を行い, B のテストでは A の, C のテストでは B の具象クラスを使ってテストを書けばいい. このテストは厳密な意味では "単体" テストでなくなるが, 単体であることよりテストすること自体が目的なわけだから, 妥協していいポイントだと思う. また, 依存元の実装が変わると依存先のテストが軒並失敗するため 単体テストで発見できない障害を早めにみつけられる利点もある.

もちろんインターフェイスが必要なことはある. たとえばデータベースやネットワーク接続, プロセス間通信など, テスト環境をつくるのが大変なものはインターフェイスの下に隠し, モックに差し替えないと不便だ. また DI コンテナの AOP 機能を使う場合は, Dynamic Proxy の都合で インターフェイスを用意する必要がある.

チーム内で開発を進める場合も他のメンバと連携する部分はインターフェイスが必要だ. 他人の実装を待ったり待たせたりすると並列に開発をすすめることができない. また, 大規模開発での役割分担をはっきりさせたい時に インターフェイスを使うのは十分割に合うだろう.

逆にインターフェイスを使えないケースもある. テストされていない既存のコードをテストする場合, それがインターフェイス指向になっていなければ 具象クラスに対してテストせざるを得ない. (まずリファクタリングするのでは順番が逆.) また C++ では性能上の理由で仮想関数を避けることもある.

なお, 自分で書くコードの内側に関しては 実装に対してコードを書くスタイルが特に重要な設計上の決定になることはない. 必要に応じて後から修正すればいい. Java や C# なら IDE のリファクタリング機能を使えば 割と簡単に実装をインターフェイスで差し替えることができる. また, ruby など動的型付けの言語には そもそも(言語機能としての)インターフェイスがない. 重要なのは実装の詳細に踏み込まず役割分担を明確にするという Programming over Interface の原則であり, 言語機能のインターフェイスはそれを支援する仕組みに過ぎない.

インプロセス DB : テストの中でデータベースを動かす

データベースに依存している部分のテストは面倒だ. セオリー通りにやるならデータベースに依存する部分を DAO (Data Access Object) として分離し, DAO をモックで差し替えることになるだろう. これは大枠では正しいのだが, やはり面倒くさい. 特に面倒なのは問合せ結果のコレクションオブジェクトを作る部分. これを手で書くのはしんどい. コードで結果をつくるかわりにファイルからテストデータを読み込んで返せば 少し楽になる. そうした仕組みを支援するミドルウェアは多い.

かわりにテストの中でデータベースを使うのも悪くない. 上記のような面倒はなくなる. 別プロセスとしてデータベースを起動しておくのは面倒なので, テストプロセスの中で動くデータベースを使うのが良いだろう. Java なら HSQL, ruby なら sqlite を使うことができる. テストの度にデータベースをクリアし, テストデータを読み込む. Ruby on Rails の単体テスト機能ではこうした仕組みが既に用意されている. Java でも同じことが割と簡単にできる.

この方法のもう一つの利点は データベースへのアクセスそのものがテストできる点にある. DAO の実装が正しいかどうかは結局どこかでテストする必要があるから, それをふつうのテストに含めるられれば便利だ.

SQL を直接書くアプリケーションでは方言の互換性が問題になり, この方法をとることができない. O/R マッパのような方言を隠す仕組みが必要になる. また, テストが増えてくるとセットアップの時間が無視できなくなることがある. そうなったらおとなしく他の戦略に乗り換えるか, テストセットを毎回実行するテストと "チェックイン前テスト" に分割すると良い.

スコープオブジェクト : setUp() と tearDown() を使わない

JUnit を始めとする典型的なテスティング・フレームワークでは, setUp() にテストの前処理を, tearDown() に後処理を書く. 私はこれは不便だと思う.

まず, setUp() には引数を渡せない. データ駆動のテストで色々なデータをテストに渡したい場合, テスト本体の中にデータの名前を書かなくてはならない. こういう時に setUp() は役に立たない. また, 異なるセットアップを必要とするテストは 別のテストクラスに分離する必要があるが. クラスを増やすのは面倒だ.

新しいテストでは, 前処理や後処理のコードをまずテスト本体に埋めこもう. 再利用をしたくなったらメソッドとして抜きだそう. (ふつうのリファクタリングですね.) そのメソッドを呼ぶのコードは高々二行くらいのものだから, あえて Template Method にする必要もない.

前処理で用意するオブジェクトが増えてきたら, 一つのクラスにまとめよう. そのクラスのコンストラクタで前処理を, デストラクタ(Java なら適当な destroy() メソッド, C# なら IDisposable#Dispose()) で後処理をしよう. このオブジェクトをスコープオブジェクトと呼んでおく. (パターンの文献を漁れば適当な名前があるかもしれない. 私はしらないけれど...) こんなかんじ:

 ...
 class HelloTestScope {
 public:
    HelloTestScope(... /* 引数 OK */...) { .. . /* setUp() 相当 */ ... }
    ~HelloTestScope() { ... /* tearDown() 相当 */ ... }

    SomeResource resource; /* 面倒なので accessor ナシ */
 };

ファイルが増えると面倒なので, まずは inner class にするなどテストと同じファイルに定義しよう.

この方法の利点は多い. まず, 前処理に引数を渡すのが簡単になる. また一つのテストの中で複数の前処理を行うことができる. 前処理の再利用のためにテストクラスの継承関係が制限されないため, 頭を使わずだらだらとテストを増やすことができるのも 個人的には快適に感じている.

Java の場合, テストが失敗した場合に destroy() が呼ばれず 他のテストの実行に影響することがある. これはこの方法の欠点だが, 失敗ケースはすぐに直せばいいと割り切ることにしている.

なお, Boost.Test にはそもそも setUp() や tearDown() がない.

引数駆動テスト : 素朴なデータ駆動テスト

一つのメソッドをテストする時, 同じコードを使いパラメタだけを変えたいことがある. そういう時は引数をうけとるテストメソッドを作り, そのメソッドを引数を変えて何度も呼びだせば良い.

...なんでこんな当たり前のことを書くかというと, 単体テストに熱意を燃やしていると FIT のようなデータ駆動のテストをしたいばかりに 外部ファイルからデータを読み込む仕組みを作ってしまったり, あるいは IDE で表示される単体テストの件数を増やしたいがために 同じようなテスト関数をいくつも作ってしまうことがあるからだ. 大袈裟な仕組みはテストのメンテナンス性を損ねてしまう. ふつうの単体テストは素朴なものでいい. こんなかんじ:

void testFooByFile(int x) {
  /* x を使ったテスト */
}

void testFoo() {
  testFooByX(-1);
  testFooByX( 0);
  testFooByX(50);
  ...
}

XML を書くよりはだいぶ楽だと思う.

シナリオテスト : ユースケースをテストにする

たとえばライブラリを開発していて, そのライブラリを使うアプリケーションは クラス A のメソッド foo の呼び出し結果をクラス B のメソッド bar で利用する ことになっているとしよう. こんなかんじ:

 X x = a.foo();
 b.bar(x);

こういうケースのテストは単体テストの範囲外, いわゆる統合テストの部類なのだが, 一方でこうしたクラスをまたぐ統合の段階でみつかる不具合はとても多い. 逆に単体テストの基本である, 個々のメソッドに対するテストでみつかるバグは比較的少い. そういう意味で, 上のようなライブラリのユースケースやシナリオを再現するテストは (単体テストでないとはいえ)書いておいて損はない. 特に専属の QA チームを持たずテストも自分でする立場のプログラマは 単体テストと統合テストを区別してもあまり有難味がない. テスティング・フレームワークで実行できるテストと それ以外のテストという区分の方が現実的だろう.

テストの目的はバグをみつけることだから, バグのみつからない単体テストを書くよりは バグのみつかる統合テストを書いた方が良い. テストされていない既存のコードベースに対する テストを書こうとして陥るアンチパターンがある: 基盤部分のユーティリティに対するテストから書きはじめたものの, まったくバグが発見できずそのうちやる気を失って挫けるというもの. たとえば 3D エンジンを作っているとして, vector や matrix の テストを書いているうちに飽きてしまう. それよりはアプリケーションが実際にエンジンを叩くのと同じようなコードを テストの上に再現し, コードの途中でのエンジンの状態(特定のオブジェクトで衝突フラグが立っているか?) や出力結果の絵(発光しているはずの位置に明るいピクセルがあるか?)を 検証するテストを書いた方が有難味はずっと高い. つまりバグがみつかりやすい.

シナリオ・テストを書く時は, "予定調和テスト" や "抜き打ちテスト" が役にたつ. 自分がステップ実行で確認したポイントをそのまま assert() として記述し, 画面の画像, ファイルの中身, データベースの新しいレコードといった シナリオの副作用も自分の目でチェックしたのと同じように assert() で検証する.

"クラッシュ・グリーン・リファクター" の障害再現テストは こうしたシナリオテストで再現すると楽な事がある. 再現条件が複雑な場合, シナリオなしにそれを揃えるのが難しいからだ.

手をかけた方がいいところ

これまではいかに手を抜くかという話をしたが, 一方で手を抜かない方がいいこともある. ただその前に, まずテストで手を抜く動機をはっきりしておく.

テストの手を抜くのは, 端的に言えばテストのコストを下げるためだ. テストも他のプロセスと同様にソフトウェア品質とコストのトレードオフがある. ただそのトレードオフが割に合うものでも, コストの絶対量が多い(敷居が高い)とそもそも テスト(開発者レベルの自動テスト)をはじめないことが多い. テストをはじめたもののトレードオフの効果を見る前に挫けてしまうこともある. (そうした言訳や残骸をいくつも見聞きするし, 私自身が過去に何度もテストの導入に失敗している.) だからまずテストをはじめて効果が得られるまでそれを続けるため, なるべく楽をしてコストを低く抑える方法を強調した. 効果を体感できればコストを支払う意欲が湧くだろう.

上で示したのは, もっぱらテストをはじめる上で楽をする方法だった. テストを続けていくと他のコストが発生する. メンテナンスのコストだ. 増え続けるテストをメンテナンスするのは結構しんどい. 巨大な仕様変更やリファクタリングがテストを破壊し, そこから復帰できなくなってしまうアンチパターンがある. このとき, テストはコストとのトレードオフに失敗している.

そうした事態を避けるための原則はソフトウェア全般と変わらない. 変化に強い, メンテナンス性の高いテストをつくること. 似たような前処理はスコープオブジェクトとして共有する. 複雑なチェックは独自の assert として分離し再利用する. データ駆動テストの仕組みをつくり, テストと本体のコードの結びつきを粗にする. レガシーコードを破棄する. こうした変更のためのリファクタリングや改善を継続的に行うことで, テストのメンテナンスコストは低く抑えられる.

トレードオフを有利に運ぶもう一つの方向は, リターンの大きなテストを優先することだ. シナリオ・テストと障害の再現テストは 細かい単体テストより断然リターンが大きい(=バグを検出しやすい). 単体テストを書く暇や気力がない時もとりあえずこうした優良テストは実装しよう. 仕様変更が破壊したテストを直しきる時間がない時も, 優良テストは優先して復旧しよう.

テスト依存症

テストを書くのは面倒だ. 複雑で時間はかかるし, 動いても地味. "テスト熱中症" なんてのはかなり誇大広告だと思う. それでもテストはバグをみつけてくれるし, 大改造や仕様変更への恐怖を薄めてくれる. だからテストがあるのに慣れてくると, テストなしにコードを書くのが恐くなる. うっかりミスを恐れ, ちょっとした修正もおっくうに感じる. どちらかというと "テスト依存症" が近いな. 私には.

洋書行

J2EE 本は読み始めから 3 週間くらい, 8.5+10.5+4.5=23.5 時間. ちょろまかしてる分もあるけど, ぬるめの本なら 25 時間強くらいで一冊読めるのか. 次は重めのを読む予定. 重めの本を一日二時間読むのは厳しいから, 緩衝材として軽めの本 ("My Job Went To India") を一冊混ぜておく. さぼった日のリカバーは重め 1 時間と軽め 1 時間というかんじで読みたい.