保守しやすく変化に強いソフトウェアを支える柱 自動テストとテスト駆動開発⁠その全体像 ~Software Design 2022年3月号「そろそろはじめるテスト駆動開発」より

今回、Software Design 2022年3月号 第2特集「そろそろはじめるテスト駆動開発 JavaScriptでテストファーストに挑戦」の第1章「保守しやすく変化に強いソフトウェアを支える柱 自動テストとテスト駆動開発、その全体像」を本サイトに掲載します。第2章以降については、本誌『Software Design 2022年3月号』電子版Gihyo Digital PublishingAmazon Kindleをご購読いただければ幸いです。

第1章では、混同されることの多い自動テスト関係の概念を、自動テスト、テストファースト、テスト駆動開発の3つの段階に分け、それぞれの効果や注意点を説明します。ソフトウェアを継続的に変更/改善するために、自動テストがいかに大きな役割を果たす開発スタイルなのかがわかるでしょう。

はじめに⁠混同されやすい自動テスト関係の概念

開発者がコードに対してテストコードを書く「自動テストAutomated Test⁠」はかなり普及しましたが、テストを書かない組織やプロジェクトもまだまだあります。書かない理由はいろいろありますが、利点がわからない、書き方がわからない、書く時間がない、コストがかかる、難しい、面倒くさい、などが挙がります。

テスト駆動開発(TDD:Test-Driven Developmentという名前は、現在では多くのエンジニアに広まっていますが、そこにはやや混乱も見られます。⁠TDD」という言葉が指しているものが人によって異なるのです。自動テストを書くことをTDDと呼ぶ人もいれば、テストを先に書くこと(テストファースト)をTDDと呼ぶ人もいます。TDDの利点を説く人の説明をよく読んでみると、自動テストの利点や、テストファーストの利点のことを述べているような場面に出会うことも多々あります。

本章では、混同されることの多い自動テスト関係の概念を、自動テスト、テストファースト、テスト駆動開発の3つの段階に分け、それぞれの効果や注意点を整理整頓していきます。

自動テストとは何か

自動テストとは、開発対象のコードに対するテストもコードとして書いて(テストコード⁠⁠、そのコードを実行することでテストの実施を自動的に行っていく取り組みのことです。たとえばStackクラスのsizeメソッドのテストコードは、次のように書けます。

def test_sizeメソッドは現在の要素数を返す():
    stack = Stack()
    stack.push('foo')
    assert stack.size() === 1

自動テストに必須の性質

自動テストの強みの源となる、自動テストが必ず満たすべき性質が2つあります。自己検証可能であることと、繰り返し可能であることです。

Self-Validating(自己検証可能)

「自己検証可能」とは、テストには成功か失敗かの2つの結果しかないこと、そしてテスト自身が人間の目を介さずに成功か失敗かを自分で判断できるということです。標準出力などにテスト対象の出力がそのまま出ており、人間が目で見て期待値と一致するかどうか確かめているようなテストは、自己検証を行っていません。多くの場合は、期待値との比較のために、テスティングフレームワークが提供する関数やアサーション(表明)が使われます。

Repeatable(繰り返し可能)

「繰り返し可能」とは、テストがいつでもどこでも同じように動くということです。昼でも深夜でも、誰の手元でもCI(継続的インテグレーション)サーバ上でも、同じように動かなければなりません。自動テストは、人の手を介さず、実行するたびに毎回同じように動かなければなりません。テスト実行後に人の手でデータをリセットしたりファイルを消したりしているなら、繰り返し可能にはなっていません。

自動テストが持つべき性質

次に、必須とまでは言いませんが、自動テストに強く推奨される性質が2つあります。独立していることと、高速であることです。

Independent/Isolated(独立している)

自動テストは互いに独立していることが望ましいです。⁠独立している」とは、テストがほかのテストに影響を及ぼさないということです。テストAでファイルに書き込み、テストBでそのファイルを読むようなテストを2つ書いてしまうと、⁠テストA、Bの順に動かすと成功するけれど、B、Aの順に動かすと失敗する」ようなテスト群ができあがります。これは独立していないテストの代表例です。あるテストの失敗が、実はそのテスト自身ではなく、ほかのテストが引き金になっているような場合、原因究明に余計な時間がかかります。

Fast(高速である)

自動テストは可能な限り高速に動くことが望ましいです。テストの実行が速いと今自分が考えたとおりにコードが動いているかどうか一瞬でわかるようになります。高速でないと実行頻度が下がり、頻度が下がると問題の把握が遅れ、その結果じわじわと自信が減っていき、コードの変更に躊躇ちゅうちょするようになってしまいます。

自動テストの効果を高める因子

より自動テストの効果を高める因子があります。誰がいつどのくらい書き、どのくらいの頻度で実行するかです。

誰が書くか

テスト対象のコードを書く本人が自動テストも書くと、効果が高まります。実装を書いた本人こそが、自動テストがもたらすフィードバックを実装に反映するハードルが最も低いからです。

いつ書くか

自動テストは、書くタイミングがテスト対象のコードが書かれた時点に近ければ近いほど効果を発揮します。テストを書き、そのテストを動かすことから、設計と実装に対するフィードバックが得られるからです。

どのくらい書くか

カバーしている範囲が広がってくると、自動テストはさらに効果を発揮するようになります。カバー範囲が広がると、書いたコードが実装時予想しなかった部分を壊してしまうような事態に気づけるようになります。テストカバレッジを数値目標にすると手段の目的化が起こりがちなので注意が必要ですが、理想的なのは「無駄なく漏れなく」書かれている状況です。

どのくらいの頻度で実行するか

自動テストは頻繁に実行するとさらに効果を発揮します。頻繁に実行することで、コードが動かなくなった瞬間、つまり欠陥が入った瞬間をとらえられるからです。

自動テストの理想的な状態

要するに、自動テストの理想は、テスト対象を実装する本人によって実装と近いタイミング(ほぼ同時)に書かれていて、対象範囲が無駄なく漏れなくカバーされており、頻繁に実行されている状態です。理想的な状態においては、今書いたコードが想像どおりに動くこと、今書いたコードが既存のコードを壊していないことが短い時間で把握できるようになります。

自動テストの効果

自動テストにはさまざまな効果があります。すぐに発揮される効果、しばらくしてから発揮される効果、組織やチームに対して現れる効果などです。それらをまとめていきましょう。

すぐに発揮される効果

自動テストには、書いたそばからすぐに発揮される効果があります。

即時フィードバックが得られる

自動テストを書くと、今書いた実装コードが想像どおりに動くことをすぐに確認できます。できると思ったことが実際にできているという自信につながります。

自動テストを書くと、今書いたコードが想像どおりに動かないこともすぐにわかります。軽微なミスに、すぐに自分で気づけるので、安心感があり、自信の喪失を防いでくれます。

「やればできる感じ(自己効力感⁠⁠」が開発生産性を高める。これは単純ですが大事な効果です。

デバッグを大幅に軽減する

自動テストがあると、今書いた実装コードがどこかを壊したらすぐに気づけます。このようなテストを回帰テストといいます。先ほどまで成功していたテストが失敗したときは、直前の変更で欠陥が入ってしまったとわかります。直前の変更内容であれば覚えていますし、すぐに直したり方向転換したりするのも容易です。

自動テストは、コードを書く時点で欠陥が入ることを防いでくれます。これによってデバッグの時間が圧倒的に削減されます。欠陥混入率の低下による圧倒的な生産性向上、これは非常に大きなメリットです。

テスト対象の理解が深まる

自動テストでは対象のコードが具体的にどう動くべきなのかを記述していくので、テスト対象をより広く深く考えるようになります。具体例を考える過程で、仕様のあいまいさや潜在的な欠陥にも気づきやすくなります。

設計改善のきっかけになる

実装とテストのタイミングが近いと、テストが設計改善のためのフィードバックとして働きます。⁠テストが書きにくい」というのも大事なフィードバックです。テスト対象が密結合や低凝集という状態になってしまっていることを示唆しているからです。

実装者本人が実装と近いタイミングでテストを書くと設計変更のハードルが下がり、現状の実装に対して無理やりテストを書くのではなく、実装のほうを、テストを書きやすい設計に変更できます。テストを書きやすい設計とは、⁠責務が明確で、結合度が低く、凝集度が高く、決定的deterministicな動作をする」という、一般的にソフトウェアが備えていると良い性質を持った設計ということです。設計上の問題を早期に解決することで、手戻りを減らすことができます。

しばらくしてから発揮される効果

自動テストには、すぐに発揮される効果だけでなく、しばらく時間が経ってから発揮される効果もあります。

記憶力や把握力の限界を補う

人間の記憶力は頼りないもので、数週間前に書いたコードのことすらよく覚えていません。⁠あのとき動いていたコードが、今もそのとおり動かせるか、動いているか」がわかりません。

自動テストが書かれていれば、少なくとも当時の想定どおりには、今この瞬間にもソフトウェアが動いていることを、ごく短い時間で確認できます。自動テストが人間の記憶力と把握力の限界を補ってくれるのです。

生きた詳細なドキュメントになる

ドキュメントでよく問題になるのが、実際の動きとの乖離かいりです。たくさんの変更がソフトウェアに入るうちに、ドキュメントの更新が追いつかなくなってきたのでしょう。

テストコードは実行可能で詳細なドキュメントになります。まず、内容の乖離に気づけるようになります。ドキュメント対象の動きが変わったらテストが失敗するからです。そして、⁠動くコードこそが最も信頼できる詳細なドキュメントである」ことをプログラマーは知っています。ドキュメントという観点でテストコードの記述や整理整頓を行うと、より効果的になります。

組織⁠チーム⁠プロジェクトに発揮される効果

自動テストには、個人だけでなくチームに発揮される効果もあります。

属人性を軽減する

自動テストを書くときに使うJUnit、Jest、pytestなどのテスティングフレームワークによって、テストの書き方と実行方法が共通化されます。誰がいつ書いたテストコードであっても、読み、レビューし、編集し、実行できます。

プロセスで品質を作り込む助けになる

自動テストは、開発の過程で品質を作り込む効果があります。最後にテストして品質を判定するのではなく、最初から高い品質を常に保てるようになるのです。

自分でテストを書くと、自分が書くコードに対して責任感を持つようになります。新規機能を開発する際は、テストを書くことで安定した開発が可能になります。不具合が発生したときは、その不具合を再現するテストコードを書き、そのテストが成功するように修正を行うことで回帰テストもそろいます。コードレビューの際には、正常系だけでなくエッジケース(極端な例)や例外系に対するテストが書かれていることを確認できれば、レビューコストが下がります。

メトリクスが取れる

自動テストは良くも悪くも量を測れるので、チーム開発のメトリクスになります。テストのカバー範囲(カバレッジ)を計測すれば、今どの程度の部分が自動テストによって保護されているかわかりますし、エンドユーザーレベルでの仕様を受け入れテストの形で記述すれば、プロジェクトの進捗管理に使えます。数値目標は手段の目的化が起こりがちなので注意が必要ですが、測れること自体はメリットと言えるでしょう。

結合⁠デプロイ⁠リリースの判断を支える

理想的な状態の自動テストは、システム全体を「動く/動かない」という二値の状態に近づけます。システムの動作が二値になると、判断に使えるようになります。

システムが想定どおりに動いていることがYes/Noでわかれば、ブランチのマージや、他チームとのインテグレーション、試験環境へのデプロイ、本番環境へのリリースなどを安全に行うための判断コストが圧倒的に下がります。自動テストはmainブランチを常にリリース可能にし、頻繁で高品質なデプロイを後押しして、開発プロセスを支える柱になります。

コストメリットがある

一般的に欠陥のデバッグや修正にはコストがかかります。そして欠陥の混入から時間が経過すればするほど、欠陥の修正コストは上がっていきます。その逆も真です。欠陥が混入してから、それを見つけて修正するまでの時間が短ければ、圧倒的にコストが削減されます。

きちんと整備され、実行され続ける自動テストは、プロジェクトの期間を通じて欠陥修正やデバッグコスト削減のための効果を発揮し続け、大きなコストメリットを生みます。

最大の効果は「根拠ある自信」

開発しているシステムが自分たちの想定どおりに動いていることが短い時間でわかると、自信が生まれるようになります。ここまでさまざまな効果を説明してきましたが、根拠ある自信こそが、自動テストの最大の効果です。

自動テストが整備されていなければ、もっと良い設計が浮かんだり、改善(リファクタリング)したほうが良いところが見つかったりしても、すでに動いているコードを壊すのが怖いという不安が改善の手を止め、じわじわと保守性、開発生産性を下げていきます。

自動テストが整備されていれば、考えるとおりに動くことが確認できて、動かなくなったらすぐにわかり、誰のコードでも同じように編集できます。そこからは、いつでもどこからでも変化に適応できるという自信、改善に着手しようという勇気が生まれます。自動テストが生み出す、根拠ある自信と勇気が長期的で継続的な変更と改善を支えるのです。

自動テストの注意点

学習コストがかかる

さまざまな事例から、テストを書くコストはデバッグの大幅な軽減などで相殺され、開発速度の面ではむしろ黒字になることがわかっています。テストを書き慣れているプログラマーは、テストを書きながら開発するほうが速いことを知っています。

しかし、テストを書き慣れていないプログラマーは、テストコードの書き方を学びながら開発するので時間がかかってしまいます。

ソフトウェア開発において質とスピードはトレードオフではなく、質が上がるとスピードも上がり、質が落ちるとスピードも落ちるのが現実です。そのとき、スピードおよび質とのトレードオフになるのは、教育、学習、新しい技術への投資です。つまり、テストの書き方を学ぶには時間がかかります。

テストにコストがかかることの解決方法は、テストをやめることではありません。うまくなることです。

──『オブジェクト指向設計実践ガイド』 Sandi Metz 著、髙山 泰基 訳、技術評論社、2016年

実装から時間が経過するとテストを書く難易度が上がる

実装から時間が経過すると、テストを書く難易度が上がります。実装したときのことはもう忘れていますし、テストのことを考えて書かれてこなかったコードは、そもそもテストを書きにくい構造になっていることが多いからです。

書籍『レガシーコード改善ガイド』[1]ではテストが書かれていないコードのことをレガシーコードと呼んでいますが、そのようなレガシーコードには「そのままではテストを書きにくい構造であるため構造を変更したい。しかし安全に構造を変更するためにこそテストを書きたい」という特有のジレンマがあります。

実装から時間が経過すると腰が重くなります。⁠テストを書く」というタスクが積まれ、いつまでたっても着手されず、いつしか塩漬けされてしまうのです。

現状の追認になると効果が薄い

「実装とテストのタイミングが近いとテスト対象の設計を改善するきっかけになる」のが自動テストの効果と説明しましたが、その反対の関係も成り立ちます。

実装から時間が経過していると、すでにそのコードが本番投入されていたりほかの人が使っていたりして、設計変更のコストが上がります。すると、設計改善よりも現状維持のほうが選択されやすくなります。

進化した現代のテスト技術を使えば、テストが難しかろうと無理やりテストを書くことは可能です。テストが増えていけば、現状把握の度合いは深まりますし、回帰テストが整備されるという大きな効果もあります。しかし、テスト対象の設計や実装を変更しないなら、ソフトウェアの設計自体は改善されていないという点に注意が必要です。そのような現状追認の自動テストは動作を保証しているだけで、良い設計を保証しているわけではないのです。

メンテナンスコストがかかる

外発的なもの(仕様変更)であれ、内発的なもの(設計改善)であれ、自動テストは設計変更の影響を受けます。テストコードも解析性(理解容易性)や修正性(変更容易性)を意識しないとメンテナンスコストが上がり、変更の足かせになってしまうことがあります。

変更を後押しするべき自動テストが変更の阻害要因になってしまうのでは本末転倒です。変わり続ける要件に適応していくには、テスト対象のコードだけでなく、テストコード自体のリファクタリング(整理整頓)も常に必要です。

構造的結合に注意

テスト対象の構造に強く依存したテストを書かないように注意しないと、自動テストと実装コードとの結合度が高まる傾向があります。これを構造的結合と言います。構造的結合の度合いが高いテストを書いてしまうと、実装の変更や改善の際にテストコードのメンテナンスも必要となり、改善の足かせになってしまうことが多いです。改善を後押しするべき自動テストが改善の足かせになるのは本末転倒です。現状追認のテストコードや、カバレッジ向上が目的のテストコードを書いているとき、良かれと思って構造的結合を高めてしまうことがあり、注意が必要です。

プログラマーのテストは、振る舞いの変化に敏感であり、構造の変化に鈍感でなければいけない

──プログラマーテストの原則 by Kent Beck

品質保証としてはもの足りない

自動テストは欠陥を減らし品質を高めることを後押ししますが、それによって品質が保証されているとか、欠陥はないという誤った安心感を持ってしまうことにも注意が必要です。

「最も効果が高いのはテスト対象コードを書いた本人がテストコードを書くときである」と述べましたが、これを裏返すと、開発者主導の自動テストからは第三者検証の観点が抜け落ちてしまうということでもあります。書いた本人が気づいていない欠陥を、その張本人が書いた自動テストがすべて検出してくれるわけではありません。品質を保証するには実装者以外の目が必要で、それはピアレビュー(コードを別の人が詳細に評価・検証するレビュー)やコードインスペクション(コードに不具合がないか人の目で確認する作業⁠⁠、ペアプログラミング(1つのプログラムを2人で共同開発する手法)などで適宜補わなければなりません。この章で以降説明するテストファーストやTDDも本人がテストを書くことから効果を発揮するので、この問題は解決されません。

テストファーストとは何か

先ほど、⁠自動テストを書くタイミングはテスト対象のコードが書かれたときに近ければ近いほど効果を発揮する」と述べました。また、⁠時間が経過するとテストを書く難易度が上がる」という問題もありました。であるならば、実装の後に書くのでなく、実装を書くにテストを書いたらどうだろうか、という観点で始まったのがテストファーストの考え方です。テストファーストとはその名のとおり、実装よりもテストを先に書くプラクティスのことです。

テストファーストの効果

実装よりも先にテストを書くというのは一見奇妙なアイデアに思えますが、実際に書いてみるとそこには大きな効果があります。

必ずテスト可能なコードになる

先にテストを書くということは、テスト対象のコードは必ずテスト可能になるということです。実装時から時間が経過するとテストを書くのが難しいという問題がありましたが、テストを先に書くと、テストが書けない実装はできません。これは単純ですが非常に強力な制約です。

テスト可能なコードを書くにはテストファーストが最も有効というよりは、そうでもしないとテストは書けない、というのが現実的なところです。あとからテストを書くのが難しく、面倒なら、結局書かれないことが多いからです。テストファーストは、テスト可能なコードにたどり着く最も効果的な方法なのです。

テスト可能なコードを追い求めるとテストの書きやすい設計を意識することになり、疎結合で高凝集の設計に自然と導かれ、試験性(テスト容易性)と網羅性が上がっていきます。

テストが書ける、そろう。これはテストファーストの大きな効果です。

設計改善効果が高い

テストから先に書くとは、テストから先に考えるということです。するとそこからはさまざまな設計改善の効果が生まれてきます。

インターフェースと実装を分けて考えられる

テストファーストでは「それは何か、どうあるべきか」から考えて、次に「それをどう作るか」を考えるという順番になります。人によってはそれを振る舞い、仕様、仮説などと表現します。

インターフェースと実装を分けて考えるのが設計では非常に大事ですが、テストファーストでは実装がそもそも存在しないので、自然とインターフェースから考えることができます。

外部からテスト対象を客観的にとらえることができる。これもテストファーストの大きな効果です。

利用者の視点に立った設計を導く

テストコードはテスト対象の最初の利用者になります。⁠どう使えるとうれしいか」から考えて、次に「それをどう作るか」という順番で考えるようになります。

ソフトウェア設計において、実装者にとっての作りやすさと利用者にとっての使いやすさ、わかりやすさは一致しないことが多々あります。ではどちらが中長期的に重要かというと、使いやすさやわかりやすさのほうが重要です。ソフトウェアは書かれる時間より読まれたり使われたりする時間のほうが、はるかに長いからです。先に実装を作ってから使うという順番では、すでに書いてしまった実装を使うようにバイアスが働いてしまいますが、テストファーストではそもそも存在しない実装を先に利用することで、利用者の視点に立てるのです。

利用者の視点に立った設計を導く。これもテストファーストの大きな効果です。

テストファーストの注意点

設計しすぎのムダ(スコープクリープ)に注意

皮肉なことですが、テストを書くタイミングが実装を追い越し、テストが設計としての意味を強く持ち始めることによって、先行設計の注意点がテストファーストにも現れます。⁠考えすぎ」⁠やりすぎ」です。そしてそれは、テストを書くタイミングが前倒しになればなるほど、先にたくさんの量を書けば書くほど顕在化します。本当は不要なものを詳細に設計してしまうリスクがあるのです。これをスコープクリープといいます。

「こういうこともあろうかと」といった、さまざまな事態を予測して備えた設計はプログラマーにとって魅力的なゴールですが、その予想が当たらなければただのコストになってしまいます。テストを書くタイミングは直前がベストで、実装よりずっと前では、むしろうまくいかなくなってしまうということです。

修正性(変更容易性)にはあまり関与しない

自動テストの注意点と同じく、テストが書けているからといって良い設計であることが保証されてはいません。まだ足りないものがあります。

ソフトウェアの保守性の品質副特性[2]でいうならば、テストファーストは試験性(テスト容易性)を大幅に向上させますが、修正性(変更容易性)にはそれほど深くは関与しません。

テスト駆動開発とは何か

テスト駆動開発(TDD)はテストファーストの利点を伸ばし、欠点を補うために生まれました。設計面でのテストファーストの強みを活かしつつ、保守性を上げるためにリファクタリングが組み込まれています。またやりすぎ、考えすぎ(スコープクリープ)を避けるために、イテレーティブな開発手順インクリメンタルな設計の要素が組み込まれました。

イテレーティブな開発手順(繰り返しながら作る)

TDDは次のような「レッド・グリーン・リファクタリング」のサイクルで開発します。

  • やるべきことをざっと整理し、箇条書きのTODOリストのような形で書き出しておく
  • レッド:TODOリストから「1つ」ピックアップして、テストから書き(テストファースト⁠⁠、そのテストを実行して失敗させる
  • グリーン:失敗しているテストを成功させることに集中した実装を行う
  • リファクタリング:すべてのテストが成功しているままで実装コードやテストコードをきれいに整理整頓する
  • リファクタリングが終わったら気づきをTODOリストに反映し、次のTODO項目を選んでレッドに進む

上記のように、レッド、グリーン、リファクタリング、レッド、グリーン、リファクタリング、......と繰り返していくのがTDDの姿です。なお、TDDを実際にどう行うかは第3章をご覧ください。

インクリメンタルな設計(少しずつ作る)

TDDにおいては、一度に1つだけのことを行います。最初にすべての設計を終わらせてから実装に入るのではなく、小さく安全なステップを繰り返しながら、設計も、実装も、テストも、すべてを1つずつ積み上げていくように開発していきます。そして、必要だと思うものから順番に着手していくことで、⁠考えすぎ」⁠やりすぎ」を防ぎます。設計を常に振り返り、必要十分な設計を得たらそれ以上深追いせず、現状の設計とコードをきれいに保つことに努めます。


このようにTDDは、開発を単一の目的の小さいステップに分解して手順を定義し、テストファーストとリファクタリングを技術的な基盤に据え、イテレーティブな手順とインクリメンタルな設計を組み合わせた、無駄がなく規律のある反復型のプログラミング手法です。

テスト駆動開発の効果

プロセスの強さ

TDDのメリットは目の前への集中です。

TDDのサイクルでは、各ステップで何をやるべきかがはっきりしています。自分がいるステップを認識していれば、今自分が何をすべきかが明確になります。⁠動作するきれいなコード」という二兎を追うのではなく、レッドでは仕様をテストコードで表現することに集中し(あるべき設計を定義⁠⁠、グリーンでは失敗しているテストを成功させることに集中し(動作するコード⁠⁠、リファクタリングでは動作しているコードをきれいにすることだけに集中します(きれいなコード⁠⁠。単目的のタスクの組み合わせによってフィードバックサイクルが短時間で回る点がTDDの良さであり強みです。

TDDは「プログラミング中の不安をコントロールする手法」であるとも表現できます。複雑さを分解し、すばやいフィードバックから学びを得ながら、設計と実装を進めていく方法に自信を持たせてくれます。

保守性を高めるリファクタリング

リファクタリングはTDDの柱です。試験性(テスト容易性)はテストファーストが高めますが、修正性(変更容易性)はリファクタリングが高めます。

リファクタリングは非常に重要な技術ですが、自動テストと同じく「あとできれいにしよう」と考えていると結局着手できないタスクの典型でもあります。TDDの1ステップとして小さいリファクタリングを常に行うことによって、初めて必要十分な量のリファクタリングが行えるようになります。リファクタリングを独立タスクにせずステップに組み込んだのはTDDの画期的な点です。

楽しく⁠誇らしい

TDDのプロセスに沿って進むとき、小さな挑戦と小さな成功の連鎖が短い時間で連続して訪れます。TDDはプログラミングをゲームのような、スポーツのような、常に達成感を感じられるアクティビティに変えてくれます。

また、リファクタリングが常に行われるので汚いコードを汚いまま放置しません。自分が書いているコードに対して常にプログラマーとして誇りを持てるのは、無視できない効果です。

「身軽であることが備え」

ソフトウェアが絶えず変化にさらされる現代において設計に終わりはなく、常に設計し続ける必要があります。

変化に備えすぎると予想を外したときの影響が大きいので、TDDは変化に備えすぎるのではなく、そのときどきで必要十分かつシンプルな設計を維持することに努めます。⁠身軽であることが備え」という考え方です。⁠こういうこともあろうかと」はTODOリストに書き留めておき、コードと設計は身軽さを維持します。シンプルでかつ身軽であれば、予想外の変化に適応していくこともできます。

TDDは必要十分でシンプルなコードを維持することで、今の最適な設計から次の最適な設計へとリファクタリングで安全かつスムーズに移行し続けることを狙っているのです。TDDは結果としての良い設計ではなく、良い設計の生成プロセスに注目していると言い換えてもいいでしょう。TDDの提唱者Kent Beckによれば、良い設計とは「1.テストが成功していて、2.意図が明確に表現されており、3.重複がなく、4.最小の要素で構成されている」ことです。ではTDDは良い設計を導くのかというと、導くというのは言い過ぎになるでしょう。しかし、TDDは良い設計に近づく準備にはなります。

TDDは、設計のひらめきが正しい瞬間に訪れることを保証するものではない。しかし、自信を与えてくれるテストときちんと手入れされたコードは、ひらめきへの備えであり、いざひらめいたときに、それを具現化するための備えでもある。

──『テスト駆動開発』 Kent Beck 著、和田 卓人 訳、オーム社、2017年

TDDは、良い設計が生まれるフィードバックと改善のプロセスをプログラミングのステップに組み込むことで、答えに近づいていけるしくみだと言えるのではないでしょうか。

テスト駆動開発の注意点

TDDのメリットは「目の前への集中」でしたが、目の前への集中はデメリットにもなり得ます。目の前の問題を解決することだけに過度に集中すると、全体の視点を失った、視野が狭い設計が残ってしまう可能性があります。

TDDではサイクルを円滑に回すために大きい問題を小さい問題に分割し、小さい問題を解決するテストとコードを組み合わせて大きな問題を解決していきます。その過程で、手前にゴールを設定するために、最終的には使わなくなるかもしれない小さな機能や設計、それらのテストを重ねていくことがあります。リファクタリングをおこたってこれらの過渡的な設計成果物としてのテストとコードが残り続けるとメンテナンスコストが上がってしまいます。

おわりに

ここ10年ほどで、ソフトウェア品質の中でも「保守性」の重みが大きく変わりました。プロジェクトからプロダクトへのシフトがあり、ソフトウェアは完成させるものではなく継続的な変更と改善を続けるものになりました。言わばソフトウェア開発は常に保守フェイズとなり、継続的な開発を支える保守性の重要性がこれまでになく高まったのです。

試験性(テスト容易性⁠⁠、修正性(変更容易性⁠⁠、解析性(理解容易性⁠⁠、再利用性、モジュール性などで構成されている保守性は、以前は「現状維持力」のようなイメージを抱かれることが多かったのですが、今では前進力、改善力、推進力の源です。自動テストはそのような保守性を支える柱です。本章で説明した、自動テストのさまざまな効果が保守性を向上させ、チームの前進力となります。テスト自動化の理由としてコストメリットを挙げる方が多いですが、コストだけに目を向けると本質を見誤ります。自動テストを書くのはチームとコードが変化に適応し続ける能力を備えておくためです。

したがって、現代において、変化に適応し続け、競争力のあるソフトウェアとチームを作るためには、自動テストは必須であると言えるでしょう。自動テストを書くのが有利なのではなく、書かないのが不利になる時代になりました。

本章では自動テスト、テストファースト、テスト駆動開発の3つの段階に分けて説明しましたが、この中では、自動テストが最も重要です。実装と自動テストをほぼ同時に書くのは個人にもチームにも大きな効果があります。

それに対して、テストを先に書くか(テストファースト⁠⁠、思考プロセスに組み込むか(TDD)は、自動テストの弱点を補い、さらに開発生産性を高めるために、個人が選択する開発スタイルであると考えるのが穏当なところでしょう。

本特集を読んで、自動テストとテストファースト、テスト駆動開発を判別できるようになり、また本特集が自分の開発スタイルとして取捨選択を行うきっかけになれば幸いです。

おすすめ記事

記事・ニュース一覧