• ビジネス
  • IT
  • テクノロジー
  • 医療
  • 建設・不動産
  • TRENDY
  • WOMAN
  • ショッピング
  • 転職
  • ナショジオ
  • 日経電子版
  • 日経BP
  • PR

  • PR

  • PR

  • PR

  • PR

Java/J2EE

【連載◎開発現場から時代を眺める by arton】第2回

テスト駆動開発(TDD)が分かると従来の設計手法の問題が見えてくる(前編)

arton 2004/04/27 ITpro

 本稿では,テスト駆動開発(Test-driven Development――以降TDDと略す)について解説する。TDDは,その名の通りテストを主としてプログラムを開発する手法だ。ここで重要なのは,TDDはテスト手法ではないということだ。では何かと言えば,TDDはその名の通り開発手法なのだ。さらに正確に言えば,プログラムの開発工程を設計,実装,テストの3段階に分割した場合の最初の段階,すなわち設計を主眼とした開発手法なのである。その意味では設計手法と言い切ってもそれほど間違いではない。TDDによってプログラムの開発工程(設計,実装,テスト)がイテレーション(反復)される以上,最初に来る「設計」がTDDの主眼となることはある意味当然のことだ。

 本稿の目的は,TDDがテストのためのものだというありがちな誤解を解くことである。またそれと同時に,なぜプログラムの設計工程以降にTDDが有効なのか,逆に言えば実装の詳細を細部まで,プログラミング工程の前にあらかじめ机上で設計する従来型の手法の何が問題なのかを解説する。

 まず最初にTDDの概略を説明しておこう。TDDでは次の手順でプログラムを開発する。


  1. 実装する機能を決定する。
  2. 該当機能をテストするプログラムを書く。
  3. テスト・プログラムがコンパイルエラーとなることを確認する(まだ該当機能は実装されていないからだ)。
  4. プログラムの該当機能の枠組みを書く。
  5. テストを実行する。まだ機能は実装されていないため,エラーとなる。
  6. 機能を実装する。
  7. テストを実行する。テストを通過するまで6.との間を行き来する。
  8. 必要であれば実装を見直し,コードの重複や可読性に乏しい個所を修正する。なお,この工程をリファクタリングと呼ぶ。この工程が不要な場合は10.へ進む。
  9. 再度テストをする。テストを通過するまで8.との間を行き来する。
  10. 次の機能のテストを記述するために1.へ戻る。

 この手順はビル・ウェイクの『テストファースト交通信号(2001年1月)』で紹介された方法で,テスト・プログラム自体の問題のあぶり出しや,機能(実装単位)の粒度を考えるための判断材料を兼ねている。なお,『テストファースト交通信号』はXP(エクストリーム・プログラミング)と密接な関係にある文書であるが,本稿ではXPとTDDを切り離して論じる。

 筆者はXPは優れた開発手法だと考えているが,必ずしもXPが定めるすべてのプラクティスを忠実に採用するべきだとは考えていない。また本稿では着実な方法論としてウォーターフォール開発の実装設計以降の工程にTDDを適用するという保守的なアプローチを示す。そのため,ここではXPについて論じる必要がないからだ。

 さて以前からのIT Proの読者ならば,2002年9月に掲載されたコラム記事『テスト・ファーストなんて嫌いだ!』をご存知かも知れない。実はTDDという名称はこの記事の中にも既に出現しているし,開発の進め方についても説明されている。

 しかし今読み返すと,たかだか1年半の間に随分と様子が異なっていることがわかる。『テスト・ファーストなんて嫌いだ!』では,TDDの意義をテストを中心に据えることによるプログラムの品質向上と捉えているように読めるからだ。いや,筆者もコラム記事を書いた真島氏のレトリックに翻弄されているだけかも知れないが。それでもTDDがXPの構成要素として扱われていると読み取ってもそれほど間違いではないだろう。

 しかし,最初に書いたように,本稿ではTDDを設計手法として,XPとは切り離して考える。

 この場合のTDDの目的は,オープンシステム上のオブジェクト指向設計とオブジェクト指向言語を利用した開発において,机上での実装設計という工程の不要化(と単体テスト段階の工数削減)にある。それによって,機能設計から結合テストまでの期間を短縮し手戻りの発生や仕様の変更に耐えうる時間的な余裕を確保することである。また,それと同時に開発成果物の品質(バグの有無だけでなくプログラムの可読性やメンテナンス性を含む)を向上させることだ。

 以下,次の順序でTDDの適用について解説する。なお,個々の用語については差異があるため,ここでは筆者の用語を利用していることはお断りしておく。本稿での「機能定義」という言葉は,「外部設計(基本設計)」と「内部設計(詳細設計)」のうち入出力詳細設計(の一部)に相当すると考えられる。また本稿での「実装定義」という言葉は,「内部設計(詳細設計)」から,入出力の詳細設計を除いたものに相当する。「~仕様」という用語もそれに準じる。

 最初に従来の業務アプリケーションの開発手順について解説する。ここで重要なのは開発成果物の目的である。

 2番目に,TDDを適用することによって最初に説明した開発手順がどのように変わるかについて説明する。

 最後に,TDDがどのような開発に向いているかについて説明する。

従来型の設計手法で「実装仕様書」が必要だった訳

 従来型の開発では最初に要件定義書を作成し,そこから導かれた機能定義,機能を個々のモジュールに適用した実装仕様書の作成,実装,ユニット・テスト,結合テスト,受け入れテストという工程を取る。テスト仕様書は通常,機能定義の時点あるいは実装仕様の作成時に行われる。なお実際には機能定義と一言で言っても画面遷移のようなエンドユーザーに密着した仕様から,アプリケーション層のプロトコル仕様やデータベースの概念設計まで幅があるが,ここではモジュール分割された時点での機能仕様について着目している。すなわち,本稿で対象とするのは,アプリケーション・アーキテクチャ決定後の,個々のモジュールの機能仕様についてである。

 モジュールの機能仕様が文書化される理由は明白だ。機能仕様が実装と切り離されてかつ参照可能な状態でなければ,重複機能の検出のような実装よりの作業から,運用計画やワークフローの定義のようなモジュールの配備実行作業まで,あらゆる局面で困難となるだろう。また,機能仕様が実装から独立していなければ機能の拡張,追加が実装に縛られてしまうことになる。一言で言えば,機能仕様はWHATで,実装はHOWだということだ。

 従来型の工程では,この機能仕様が確定したところで,実装仕様書の作成が始まる。ここではCOBOLを想定して,HIPO(注:Hierarchy plus Input Process Output,構造化手法でのプログラム機能記述の技法)の使用を想定する。機能仕様から入力と出力(あるいは実行によるテーブルの更新など)は明らかなので,そこから構造をトップダウンで設計することになる。なお,本稿では実装仕様書と呼んでいるが実装設計書と呼ぶこともあるので用語には捕われないでいただきたい。

 いまどきのCOBOLであればローカル変数やオブジェクトの利用も可能だが,ここでは従来型開発にふさわしく70年代のCOBOL仕様で考えることにしよう。この場合,エラー検出時の制御,内部での処理分岐の結果といったものは該当モジュールのグローバルな状態変数で制御せざるを得ない。データとプロシージャはプログラム内の異なるdivisionで示されるため,効率的な記述(プログラムの先頭から記述していく)を考えればあらかじめ各divisionに出現する要素を実装仕様として切り出して目録化しておくことに意義がある。そうでなければ,サブルーチンの追加により思わぬデータの書き換えが発生してしまうかも知れないからだ。

 またかってはプログラムの入力はカードや紙テープを利用していたために,実装仕様の記述のほうがプログラムの作成よりはるかに短時間で完了した。これにより,たとえば1人の実装仕様作成者と10人のプログラマというような組み合わせで効率的な作業が可能となる。仕様作成(設計)とプログラミングが分離している以上,仕様書として独立させる必要があるのは自明だ。

 さらに実装仕様書は,システムの置き換え時に重要な意味を持つ。実装仕様書さえあれば,ソース・ファイルが散逸しようと,プログラミング言語を変えようと,いつでもソースコードを再作成可能だからだ。

 まとめると実装仕様書の意義は次の3点に集約される。


  • プログラムで使用する要素の目録化
  • 作業の効率化
  • ソースコードと分離した実装の記述

 これに加えて,トップダウンアプローチによるサブルーチン化された構造(HIPOのシート番号に相当),実装で使用される要素の目録化といった仕様書の記述方法は,あらかじめコード設計で定義された,モジュール名,プロシージャ名,変数名などの記号化された名称(「XA-001-0023Z」のような分類化された名称)を導出する。おそらくそれもこの工程からすれば極めて合理的な判断に基づくものだ(注:だからと言って,筆者はこの命名方法には賛成していない)。

 重要なことは,この手法は,


  • 開発の分業体制
  • 構造化言語
  • 文書化

 の3点と,(入力媒体の貧弱さに由来する)ソース・ファイル作成の困難さが存在する限りは,合理的だということだ。何をいまさらと思われた読者もいるだろうが,それは物事を自分の視点でしか見ていない。おそらく,システム開発経験を持っている読者の多くが「ああ,あの訳のわからない慣習はそういう意味だったのか」と感じているはずだ。

テスト駆動開発(TDD)で変わる実装設計

 それでは次に,TDDを取り入れた場合,この工程がどう変化するのかを見てみよう。
実装工程にTDDを取り入れた場合であっても,最初に要件定義書を作成し,そこから導かれた機能定義までは変わらない(注:本稿で説明している開発手法では,XPとは違い「オンサイト顧客」は想定していないことを思い出して頂きたい)。要件のヒアリングと分析,まとめ,そこから始まる機能の洗い出しと定義については,基本的な工程の変化はないからだ。

 もちろん,UMLを駆使して,たとえばユースケース図を書くといった変化はあるかも知れない。しかし,それは文章より図示されているほうが読みやすい,という程度のことである(注:読みやすい事が決定的に重要となる局面もあるが,それは本稿の範囲では触れない)。もちろん分析手法が異なると言うことは可能であるが,ここで着目しているのは開発成果物であるからこれについてもここでは触れない。したがって,同様に,機能定義についても文章として記述されるのか,HIPOの総括ダイアグラムの流用になるのか,あるいはUMLのコンポーネント図となるのかは問わない。

 また,モジュールという用語は必ずしも粒度が明確ではない。そのため誤解を招く可能性があるだろう。ここでは対応している構造化での開発工程に合わせている事から,OLTP(オンライン・トランザクション処理)システムであればトップレベルの1トランザクション境界くらいの粒度を想定してモジュールと呼ぶことにする。

 さて,機能仕様が完成した時点以後,開発方法は様変わりすることになる。
次の手順は,ソースの記述だ。もっとも人によっては簡単なクラスダイアグラムをメモとして記述する場合もあるだろう(筆者はこのスタイルだ)。

 この時,最初に記述するソース・ファイルは,ターゲットとなるモジュール「ではない」。そのモジュールのテスト・プログラムだ。すなわち,ここからTDDが開始されることになる。

 もちろん,いきなりモジュールの機能そのもののテストを書くことはモジュールの粒度によっては(ということはほとんどすべての場合において)意味を持ち得ない。その場合は,そのモジュールが要求される機能の実現に必要となると想定されるサブモジュールのテストとなる。すなわち,この開発工程ではボトムアップアプローチが利用される。

 後は,設計―実装―テストの反復によって最終的な機能の実装へ進むことになる。この工程については本稿の冒頭に挙げた通りだ。

 従来型の開発での実装仕様作成者とプログラマの分業はここではどうなるのだろうか。結論から言うと,「ない」。ただし,プログラマの経験などに応じて,モジュールの粒度を変更するといった作業が必要になる可能性がある。また,共通で使用するモジュールの作成といった作業もある。そのため,全員が同等な開発作業を行う必要はない。また,ここで可能であればXPからペアプログラミングを採用して熟練者によるテスト・プログラム作成とコード・インスペクションとメンタリング,非熟練者によるプログラム作成という分業を行うことは十分に有効であろう。工数が許すのであれば,開発しなければならない全モジュールのうち難易度が中程度のものを利用して,最初はペアプログラミングで開発するのが良いのではないかと想像される。しかしこれについては実際に行ったわけではないので確証があるわけではない。

 TDDでの最終的な開発成果物は,複数のテスト・プログラムと複数のソース・ファイルだ。Javaで記述した場合であれば,Javadocと呼ばれるソース内のコメントをHTMLとして抽出したものが付属することになるだろう。

TDDで単体テスト工程が不要になる訳ではない

 なお,TDDが開発手法であってテストが主眼でないという点から,従来型開発プロセスで実施される単体テスト(以降,単に「単体テスト」とあれば,それは従来型開発プロセスでの単体テスト工程を指す)が不要になるわけではないという点については指摘しておくべきであろう。単体テストの適用時の注意点については後で再度触れることになるが,ここでは,TDDが単体テストの代わりにはならない理由を説明しておこう。

 TDDが単体テストを兼ねることができない理由は,実行されるテストの内容が異なるためである。

 TDDで実行されるテストは次の3点である。


  1. プログラムが実装した通りに動作するかどうかの確認
  2. 実装の部分的な追加に動作に支障を来たす副作用がないことの確認
  3. 実装の過程で発見されたコードの重複や無駄のリファクタリングに動作に支障を来たす副作用がないことの確認

 一方単体テストでは,モジュール全体の機能について正常系と異常系の両方についてテストする。通常1トランザクション境界に相当するモジュールは3から4個くらいのクラス(5モジュールを10の独立したクラスと20の共通クラスという実装となることもあり得る――設計に依存する)として実装することになるが,単体テストの対象はモジュールであってクラスではない。逆にTDDでは単体テストが対象とする粗粒度のテストは開発の最後にならなければ必要とならないし,細粒度のテストと異なり必ずしも必要とはしない。また,単体テストでは負荷テストやモンキーテスト(無茶苦茶な入力を試す)もモジュールの種類によっては必要となるだろう。だが,これらのテストはTDDでは行わない。

 結局のところ,TDDと単体テスト工程でのテストではテストの目的が異なるのである。TDDでのテストの1番の目的は,ある機能を実現するためにはどのようなプログラムの構成が必要かを具体的に考えるためであり,外部からの呼び出しを最初に考えることによって可読性が高いソースを作成するためだ。一方の単体テストの目的はそのモジュールにバグがないかの調査である。

可読性が高いコードとは

 TDDの目的である「可読性の高いソースの作成」という点については注釈が必要であろう。なぜなら実際のプログラムに触れたことがなければわかりにくいと想像されるからだ。以下に表層的ではあるがわかりやすい例としてコード片を示して説明する。


可読性が低いコード
if (y0001_02()) { // この処理が何かはソースからは不明

可読性が高いコード
if (isAccountExist()) { // 口座の存在確認をしていることが読解可能なコード

 この差はテスト・プログラムを記述することにより明らかになる。

account.accountNumber = “12345”;
assertTrue(account.y0001_02());

とテストに書いて平然としていることは,構造化開発で,実装仕様書を無視して勝手な変数名やプロシージャ名を付けるのと同じことだ。自分勝手なルールを採用しているかどうかは,テストコードをインスペクトすればわかる。この例は最も表層的なものだが,同様にテストコードを参照すれば,対象となるモジュールがどのようなインタフェースを持つのかは明らかとなる。

ここでソース・ファイルの可読性が要求されるのは,異なる開発者への引継ぎや将来的なメンテナンス性を想定すれば当然のことだ。異なる開発者はJavaで記述されたソース・ファイルは読めても日本語の仕様書は読めないかも知れないし(優秀なプログラマが日本人とは限らない),外部から呼んだ助っ人であれば,システム独自のコード設計に基づいたソース・ファイルよりも,はるかに一般的な言葉を利用したソース・ファイルのほうが短時間のうちに正しく読み取る可能性が高いからだ。ソース・ファイルの可読性の高さは,ソースのメンテナンス性の重要なファクターである。


 関連して触れると,コード設計自体も,ことプログラムに適用する場合には検索技術に期待できなかった過去の遺物と見なせる。ソースプログラム上に出現する自然言語に基づいた命名を,現在の開発ツールは問題なく処理するからだ。したがって,機械にとっては処理しやすいかも知れないが,人間にとっては判じ物に過ぎないコードの直接的な利用はやめたほうが良い。読書感想文や書評にISBNコードを書かれても普通の人間には通用しない。またISBNコードが知りたければ検索技術があるから,書名と出版社,著作者が明らかならば必要となった時点で検索すれば十分である。どうしてもコードが欲しくて我慢できないのであれば,同じように対応表を別にデータベース上で管理すれば良いだけのことである。これには,現在のプログラミング言語が階層化された名前空間をサポートする ようになったことも影響している。ファイル・システムを持ち出すまでもなく,コード による分類から、自然言語を利用した分類への移行は必然である。

TDDで工数が増える?

一見するとTDDを採用することは,工数が増加するように感じられる。成果物のファイル数は倍になるからだ。だが,既に見たように,ボトムアップアプローチで着実にプログラムを作成するには,早いうちに個々の処理を完成させていくことが重要となる。そのため,TDDを採用することで最初からテスト(実行)ができることは大きな強みである。
結局TDDによる開発は実装全体を通すと,


  • 実行結果を確認しながら記述するため,後からの無用な書き直しが不要
  • 細粒度のテストによって着実に積み上げが可能
  • プログラム全体は小さなモジュールから構成することになるため,個々のモジュールのコンパイルのような作業に必要となる時間は短縮

といった複合的な要素から,必ずしも工数の増加を意味しないのである。

 このようにしてモジュールが完成した時点で,単体テストを適用することになる。先に述べたようにTDDによって単体テストが不要になるわけではないからだ。

 なお,TDDを適用した場合(かつ正しく実施された場合),単体テスト工程は極めて短くなる。なぜならば,正常処理パターンおよび,典型的な障害パターンについてはTDDによって最初から解決された状態として作成されていると前提できるからだ。また,プログラムの構造が正しければ,モンキーテストなどにも高耐性が期待できる。そのため,単体テスト時の摘出不良目標件数を従来の統計から求めても全く意味を持たない。この点を間違えると,摘出不良の成果が上がらないことにより無用な単体テスト工数が必要となるので注意すべきである。

 正しくTDDを適用していれば,基本的に単体テストは初回のテストでパスするはずである。そのため,この工程での工数の短縮が期待できるのである。同様に結合テスト工程においても,正常系や機能仕様で想定されている異常系については初回のテストでパスする可能性は極めて高い。

 以上をまとめると,実装仕様書のかわりにTDDを採用することの意義は次の3点に集約される。


  • ボトムアップ開発での早期問題発見
  • 実装仕様作成工程の省略と単体テスト工程の通過率向上による作業効率化
  • ソースコードの実装成果物化

 付け加えると,テストしやすいプログラムを考えることでモジュールの結合強度が弱まり再利用性が高まるといった余禄もあるのだが,こういったオブジェクト指向設計におけるTDDの意義についての論考はここでは置いておくことにする。(後編に続く




筆者紹介 arton


謎のITエンジニア。RTOS上でのデータベースエンジンを皮切りにミドルウエアやフレームワークとそれを利用するアプリケーションの開発を行い現在に至る。専門分野がある業界に特化しているために逆にメインフレームクラスから携帯端末まで,その時の需要に応じてダウンサイジングしたりアップサイジングしたりしながらオブジェクトを連携させていくという変化に富んだ開発者人生を歩んでいる。最近はもっぱらJ2EEが主戦場。著書に『Rubyを256倍使うための本 邪道編』『Visual C# .NETによるWebプログラミング入門』共著に『J2EEプログラミング講座』(いずれもアスキー刊)などがある。




あなたにお薦め

連載新着

連載目次を見る

今のおすすめ記事

ITpro SPECIALPR

What’s New!

経営

アプリケーション/DB/ミドルウエア

クラウド

運用管理

設計/開発

サーバー/ストレージ

クライアント/OA機器

ネットワーク/通信サービス

セキュリティ

もっと見る