2008年12月:この文章はレガシー化しています(リンクもほぼ死んでいることに注意)。本ページは、http://mockobjects.com/wiki/DevelopingJdbcApplicationsTestFirstを勝手に翻訳したものです(2004/2/20にフリーマンに了解を得たとは言え向こうは日本語は読めないので翻訳の正当性が保証されるわけではない)。公開物に対するリンクと勝手に翻訳は一見似ていますが、そこには大きな差があります。それは翻訳が正しいかどうかの保証がないっていうことです。したがって、この翻訳を鵜呑みにせずに、原文も参照されることを勧めます。ここでは強い意志をもってできるだけ意訳するようにしています。僕にとっては日本語の文章記述練習を兼ねています。透過的にフリーマンの記述を読みたければ、原文は読みやすいので、そちらをあたってください。MockConnectionのように、最新のMockObjectsではここに記述されたメソッドは使えなくなっているようです。実際に利用可能なメソッドはJavadocを生成して確認してください。JDBCのように利用するオブジェクトがすべてinterfaceで定義されているものは、EasyMockなどの利用も検討してみてください。Freeman著はじめにほとんどの開発者がサードパーティ製のコンポーネント(たとえばデータベースとか)を利用するソフトウェアのユニットテストは大変だと感じている。原因はたいていの場合、ユニットテストから依存性を排除するのがとっても面倒だからか、さもなければ、動作が遅くなるからだ。僕はこの文書で現実のデータベースを一切使わずに、JavaのデータベースインターフェイスのJDBCが使えるってことをお見せしよう。モックオブジェクトに基づいたアプローチがいかにより正確なユニットテストをもたらすか、そして僕はそうあって欲しいと思っているんだが、現実のデータベースを使うよりももっと整合性が取れたコードをかけるかを見せるつもりだ。テストファーストなプログラムでモックオブジェクトを書くには2つの重要なスキルがある。ひとつは、どんなテストが必要かを見極めて、オブジェクトのモック実装にそれを反映させること。次が、モックオブジェクトに本物の動作を再実装しないことだ。モックが複雑になってしまったら、モックの実装とテスト対象のコードの両方を見直したほうが良い。それはオブジェクト間の結合があまりに密になっているってことだからだ。ちなみに、これからお見せするコードとモックオブジェクトライブラリはhttp://www.mockobjects.comからダウンロード可能だ。モックオブジェクト初心者だったらサイトのドキュメントセクションに行って文書を読んでくれ。最初に例のストーリーを説明しよう。僕らのカスタマーが僕らにメーリングリストシステムを発注したと考えてくれ。誰でもリストに参加するのも退会するのも自由だけど、オウナーだけがメンバーを見れるしメッセージを送ることもできる、そんなシステムだ。誰でも名前とメールアドレスを登録できるもし会員ならメールアドレスを使って退会できるオウナーは会員リストを参照できるオウナーは全員にメッセージを送れる会員登録とりあえず、アクセス制御は周りのシステムが固めてくれているっていう前提で、リストを管理するMailingListクラスを開発しよう。インサートをうまくやるには、次の条件が必要だ。会員のメールアドレスと名前を元にしてインサートのパラメータを設定インサートのために1回ステートメントを実行ちょうど1回ステートメントをクローズする。[...]}どうやら、コネクションとステートメントのアサートが必要らしい。それから会員を登録するにはメーリングリストオブジェクトにコネクションオブジェクトを渡す必要もありそうだ。ここで使っているデータベースオブジェクトの名前を変えて明示的にモックだってことをわかるようにしたほうが良いかも。テストはこんな感じになる。[...]}さてと、testAddNewMember()はSQLExceptionのスロー宣言をしてるわけだ。これはもちろん、もしSQLExceptionがスローされたらJUnitフレームワークがキャッチしてエラーをレポートするってことになる(僕らの環境をぶち壊してね)。厳密には、僕らがそいつをキャッチして(コーディングミスがあるわけだから)失敗としてやる必要があるわけだが、JUnitに任せてしまうのは手軽だしたいていのプロジェクトには十分な処理だよね。次のメソッドに取り掛かろう。mockConnection.setupPreparedStatement(mockStatement);}ここまでできたらMailingListの実装だ。最初のテストをパスさせるために、テスト側が設定した期待値を設定しよう。}}もう気付いているだろうけど、この実装はステートメントを確実にクローズしているわけじゃないね。この最初のテストが自分の殻に閉じこもっているってことだ。1つのテスト——つまりリストに会員を登録することだけ——しかしない。正しくデータベースをインストールして、テーブルを用意して......なんてまったく必要なし。ま、こんな状況はあるよね。プロジェクトはとっくに始まっているっていうのにデータベースの選定が遅れてる、そんな場合でも大丈夫ってわけだ。データベースプログラムのテストを作るには2つの視点から考えなきゃならない。僕らがデータベースに「何を与える」かと、データベースが「何を欲しがってるか」だ。このアプローチはこれを区別するんだな。システムがでかくなってもエラーをピンポイントで発見できるわけだ。全部ひっくるめた統合テストは必要だよ。それは大きなモジュールでの受け入れテストみたいにするべきだろうな。このテストスタイルをうまくやるには、オブジェクトのやりとりが必要だ。今の例だと、僕らはコネクションオブジェクトを検証する必要があったから、それをメーリングリストオブジェクトへ引数として与えてたよね。現実問題として、ほとんどのシステムはパフォーマンスをよくするためにコネクションをプーリングしているわけだから、何かやらせるたびにコネクションを渡してやるって言うのは正しいアプローチなんだ。僕らはモックを使ったテストファーストをやることで、正しいデザインをしてしまうことになるんだよ。話し合った結果、僕らのカスタマーはそんなことは認める必要はないから、エラーメッセージを表示しろって決断してくれた。それじゃ僕らもMailingListExceptionをスローすることに決めよう。重複を検出したら例外をスローしたかを検出するテストが書けるじゃないか。mockConnection.verify();}このsetupThrowExceptionOnExecuteっていうメソッドは、モックSQLステートメントがexecute()を呼び出されたら例外をスローするように作ってある。僕らはあらかじめ仕込みを入れたモック実装のステートメントをオブジェクトに与えることができるようなコーディングスタイルを取っているからこういった芸当ができるわけだ。相変わらずコネクションとステートメントを使って正しいパラメータが使われているかとか、executeとcloseが1回しか呼ばれないこともチェックできるし。このテストはちゃんと例外をキャッチしてMailingListExceptionに変えてスローするかも確認できるんだ。しなかったらfailを呼び出してアサーションエラーがスローされるわけだし。複雑になったけど、相変わらずテストは明確だし自己説明的だよね。テストがパスするように実装も修正しなくちゃ。}}僕らはステートメントの利用中に失敗しても良いようにfinallyブロックを追加してちゃんとクローズするようにしたぜ。ここがおもしろいとこなんだけど、このコードは今度はもう1つのテストもパスするようになったんだ。mockStatement.verify();}これはコネクションオブジェクトの呼び出しが失敗したら、ステートメントに対してexecuteやcloseを呼び出さないことをチェックしているんだ。相変わらず正しいprepareStatementの呼び出しは要求してるけど。確かに例外のスローと管理ってのはテストの中でも飛びっきりに厄介なことだとは思うよ。実際に失敗したときにちゃんとリソースのクリーンアップをしてないコードとか、してるのは良いけれど妙にごちゃごちゃやっているコードとかを見かけるけどさ、本物のライブラリを使ってたらすごく面倒だったり、そもそも無理だったりする例外の生成をシミュレートする方法を使って、少しずつテストファーストでプログラムするってことは、デベロッパーが正しくエラーをハンドリングするのにとっても効果的だってことなんだ。どこまでちゃんと処理するかってことはアプリケーション次第なのは事実だけどね。一発勝負のユーティリティーだったらクリーンアップなんてどうでも良いかも知れないけど、ずーっと動き回ってるサーバーサイドアプリケーションだったらちゃんとやらなきゃだめだよ。ひとつ、デザイン上のポイントがこの例にはあるんだよね。メーリングリスト例外(システムは正しく動いているけど、正しくなく使われた場合)とSQL例外(こっちはシステムのエラーだ)を僕がちゃんと分けてるってことだ。アプリケーションがでっかくなると、例外ハンドリングをうまくやるために、このテのSQL例外をアプリケーションの特有例外に割り当てたくなってくるってことだ。ま、こいつはどうでも良いことかも知れないけど。次にすることはメールアドレスを元にして、誰かをリストから削除する作業だ。削除の成功を判断するテストはこんな感じ。mockStatement.verify();}ここでも、僕らは正しいパラメータを使って正しいメソッドを呼び出しているかをチェックしている。削除に成功すれば0より大きな更新件数が返ってくるはずだから、あらかじめステートメントオブジェクトに戻り値を設定しておくことにする。僕らは更新件数を0にすることで、リストに存在しない会員の削除のテストもできる。この場合は、デリート要求に指定されたメールアドレスに一致する行がなかったってことだ。mockStatement.verify();}removeMemberメソッドの実装は以上2つのテストを通るようにする必要がある。}}そろそろリファクタリングをすべきだろう。ユニットテストに繰り返しが見えてきたからね。会員リストに何かしようとしたらモックSQLオブジェクトに特定のセットアップが必要になる点と、いつも2つのオブジェクトの検証をしてる点だな。それじゃ、リファクタリングしてヘルパメソッドに追いやることにするとしようか。mockStatement.verify();}これで既存のテストをシンプルにできるし、可読性も上がったはずだ。アプリケーションが大きくなってきたら、新しいテストの追加コストが下がるってことだ。作ったテストから利益を回収できるからね。最初の時点ではその利益は知的作業ってやつ——コードの切った貼った直したってやつだ。次の段階ではテストをリファクタリングして重複を削除。テストをメンテしたりリファクタリングしたりすることは、作ったコードをメンテすることと同じくらい重要だってことだ。テストはコードの成長や変化に追随できるようにアジャイルじゃなきゃならないんだ。つまるところ、僕らはバランス感覚ってやつを磨かなきゃならない。テストが読みにくくなるまではリファクタリングしてはだめだよ。検証処理について言えることだけどね。今の例で、わかりやすい名前のメソッドへ移動した2つの検証オブジェクトはそのままでも十分にわかりやすかっただろ。いうことは、本当はテストケースの中に残しておくべきだったかも知れないな。そのほうがテストを読んだ時に何を検証してるかはっきりするからね。会員リストの表示さて次にやるべきことは、登録されている全会員の表示だ。ここで最初に問題となるのはどうやってちゃんと全員を参照したかを調べる方法だ。そこで全会員のリストを処理するオブジェクトを作ることにした。これをListActionととりあえず呼ぶことにしよう。次にモックデータベースをセットアップしてListActionオブジェクトにMailingListオブジェクトが正しく処理しているか検証させることにしよう。最初の単純なテストは1人しか登録されていないリストを使ったテストだ。僕らが確認しなきゃならないのは、ListActionオブジェクトが正しい会員情報を受け取ったかという点だ。ResultSetが正しいカラム名で呼び出され、next()が2回呼ばれるということ。それからいつもどおり、ステートメントの作成とクローズ、それからクェリーの実行が期待通りかという点だ。いうわけで最初のテストの実装はこうだ。mockListAction.verify();}ここでのMockSingleRowResultSetの期待値には2つの目的がある。クライアントの呼び出しが位置またはカラム名のどちらにしろ正しいカラムに対して行われているかのチェックだ。この2つの機能は分割すべきじゃないかっていう疑問を持つかも知れないが、ExpectaionMap(訳注:呼び出しパラメータに対して返送する値を格納するマップ)を実装しなきゃならないくらいパターン化してるんだからしょうがない。一方のListActionクラスの役割は会員の名前とメールアドレスのリストの期待値を持つことだ。ここでこのテストに対応するコードを示せば、ListActionのこのメソッドの大体の構造もわかるだろう。最初のテストはResultSetの正当な行から正しい値を抽出しているかを証明するものだ。このテストが完成したら、僕らは他の観点からのテストの記述に集中しよう。正当な行がもっとある場合とか。いったテストでまた抽出が正しいかを検証するのは無駄だよね。会員数が変わった場合のテストだったらlistActionオブジェクトを必要な回数呼び出すだけで済むはずだ。値の正当性の確認はもういらない。こんな感じでやっていけば、それぞれのテストは厳密だし、しかも互いに直交しているから、失敗した場合に原因を探すのも簡単になるわけだ。テストケースもモックオブジェクトも作るのが簡単になってくる。余分なことをしないわけだからね。2人の会員をリストするテストは、名前とメールアドレスを期待値に設定しないで済ませている。このテストではカラム名とダミーの値を設定するけど、これはResultSetから値を取り出した場合にキャストエラーになるのを防ぐためにやっているだけだ。このダミー値で返送する行数も決まるわけだ。僕らがまじめに設定しなければならない期待値は、各メソッドが正しい回数だけ呼び出されるかだ。mockListAction.verify();}ResultSetが1行も返さない場合のテストは単純だ。行のセットは一切無しにして、モックListActionに対しては呼び出しが行われないことを期待値として設定すれば良い。この点から、モックResultSetをヘルパメソッドに渡してセットアップを任せることにしよう。していない(MockObjectsのexamplesのソース上も)。僕らのapplyToAllMembersの単純な実装ではこれらのすべてのテストをサポートできる。言え例外管理についても見る必要があるのは明らかだ。いうわけでResultSetが失敗した場合のテストを追加してうまくできるか見てみる必要があるだろう。そのためには既存会員の追加のテストの時と同じようにやるのが良さそうだ。ListActionオブジェクトの呼び出しをtryブロックに入れて、期待値がスローされなかった場合には失敗するようにしてみよう。ResultSetから値を取得しようとしたら例外をスローするように設定しておこう。mockListMembers.verify();}ここで重要な点は、applyToAllMembersが失敗した場合の振る舞いがきれいに定義されているってことだ。レコードの読み出しに失敗しているのに処理を続行したりしてはダメだ。テストファーストで開発することとコードの利用者にテストプログラムを公開することは、これまで作られてきた中の最高のドキュメンテーションよりも、そのオブジェクトをどう使うかについての明確な説明になるんだ。ここまでの実装ではテストをパスできないってのは、finally節が抜けているからだ。ここで実装しておこう。}}コネクションがステートメントを作れなかったり、ステートメントがクェリーを実行できなかった場合のエラー処理が正しいかをチェックするテストも追加しておくべきだろう。サーバーサイドの場合、こういったエラー処理のバグはリソースリークの元になるし、このてのバグは実運用を開始してしまうと調査することも厄介になるからだ。これでMailingListオブジェクトは与えられたListAction型のオブジェクトを使ってすべての会員を処理できるようになった。次に各会員の詳細をXML文書(これを処理するのはListDocumentというアクション開始時に与えられるレンダリング用オブジェクトだ)に追加していくConvertToMemberNodeクラスを実装して結果を表示すべきだろう。}}もちろん、この新しいクラスは別の専用のユニットテストでテストされるわけだ。オブジェクトの関連をテストする場合の共通の難関はテスト環境の構築コストだ。各テストについて何を検証するのか、必要となる最小のモックオブジェクトのセットはどれか、といったことをきちんと考えればこのコストを抑制できるってことを、今見せたようなテクニックは示しているんだ。各テストが直交するように、テスト対象毎に異なる角度からテストするってことは、なかなかの優れものだ。テストはより精密になる。エラーの発見は容易になる。小さなテストは実装しやすいし、読むのも簡単だ。モック実装はそのテストにより特化できるし、実装はよりシンプルになるわけだ。別のアプローチとしてスタブJDBC実装を使うって方法もあるし、実際のデータベースをとりあえずコピーして使うって方法もある。ユニットテスト初心者や既存のコードベースの改造時のテストには良い方法だよね。モックオブジェクトを使えばさらに上をいけるんだ。セットアップは安上がり。テストのメンテは楽だし実行も素早い異常検出時にはすぐ失敗してくれる。他の方法だと全部走りきらなきゃわからない振る舞いの検証が可能だ。最後の仕事は、登録された全員にメッセージを送信する要件を片付けることだ。こいつをやっつけるのにはListActionインターフェイスの別の実装を使うことにしよう。ここではAddMemberToMessageクラスだ。ここでやらなきゃならないデザイン上の選択は、メッセージング例外をどう処理するかってことになる。中断するか、送信に失敗したメッセージを集めておいて最後にリトライするかとかだ。ここではAddMemberToMessageクラスの呼び出し側がMessageオブジェクトのセットアップと、最後の送信を受け持つように作ってみた。}}MessagingException例外はチェック例外だ。ここでキャッチしてから伝播させなきゃならない。ListActionインターフェイスを汎用的なままにしておきたいから、MessageExceptionをMailingListExceptionに変換して代わりにスローし直すってわけだ。これで新しい失敗状態を作ってしまったぞ。MailingListException発生時の処理を検証するユニットテストを書くわけだ。mockListAction.verify();}呼び出せ。テストファースト開発に対するこのアプローチは不必要に複雑に見えるかも。そりゃ単純にMailingListから会員のコレクションを取ってくるほうがシンプルに感じるよね。そいつはわからんでもない。いったコードの記述方法は柔軟なんだよ。後からメッセージエラーを無視して構わず送信を続けるってやり方に変更しなきゃならなくなったとしようじゃないか、AddMemberToMessageクラスが送信エラーになった会員を集めて送信完了後に処理するようにできるよね。同じようにメーリングリストが肥大化した場合を想像してみよう。その肥大化したリストを格納したコレクションを作る代わりに送信対象を幾つかの小さなリストに分割して処理することもできるよね。ところが、もし会員リストを直接もらうように作ったとすれば、その時点で個々の会員を追加するアプローチのListActionの実装を作らなきゃならなくなっちゃうじゃないか。まとめこの例では、個々のユニットテストの分離とモックオブジェクトを使うことによる外部リソースへの依存の回避が、どれだけ可能かを示したつもりだ。重要なことは、ユニットテストに対するこのアプローチは、柔軟な上に、システム例外のテストを含めてテストしやすいスタイルでコードを書きたくなるってことなんだ。アプリケーションが大きくなれば、通常のテスト用のインフラはモック実装を使わざるを得なくなるし、新しいテストの追加コストは実質的には下がっていくものだ。僕らの経験から言わせてもらうけど、複雑な新しいライブラリ(たとえばJDBCとか)を使うと最初のうちはテストをなかなか書けないものなんだけど、モック実装はすぐにそれを解消するんだ。実行時プロクシやスタブメソッドを生成するIDEを使った場合にはね。完了お気づきの点はどんどん修正してください。追記(arton)モックオブジェクトはまだ進化中です。MockObjectsはますますJDKのモック実装を充実化していますが、この記事の著者のスティーブフリーマン(Blog)達(実際にはナットプライスとはフリーマンの弁)のプロジェクトjMockでは、自分のオブジェクトのモック化に対する別のアプローチを開始しています。IoC(またはデペンデンシインジェクション(角谷さんの訳))といった、オブジェクトの依存関係を静的に決定せずにコンテナに任せるアプローチも、モックと関連してきます。