💬

TDDについて整理してまとめてみた

2021/05/11に公開

TDDに関する議論には多くの混乱が見られます。現在は議論が落ち着いているようですが、あまり情報がまとまっていないのでTDDの導入にいまいち踏み切れない方も多くいることでしょう。そこで、そもそものTDDの定義から関連する議論の要点まで、TDDに関連する情報を整理しました。

TDDの定義としては、

「TDDはテストファーストの上に RED / GREEN / REFACTOR のサイクルによる設計・実装手法を加えた開発手法である」

という表現で大きな異論はないかと思われます。

TDDの効果に関しては、TDDの実践によって以下のようなことが起こると言われています。

  • 十分な量の自動テストが存在する状態になり、以下のような恩恵を受けることができる
    • エラーの発見や防止がしやすくなる → プロダクトの品質の指標の一つである不具合発生率の低下に貢献する
    • リファクタリングのリスクを減らし、リファクタリングを行う意思決定の助けになる
    • 充実した自動テストがAPIのドキュメント代わりになる
  • 大きな機能や複雑な機能に対しても小さなステップを一つ一つ着実にこなしていくという点で完成までの道筋が明確になり、自信をもってコードを書き進められるようになる
    • 実装のしやすさにもつながり、時には開発速度の向上に寄与することもある
  • テストの記述を通して抽象化に関する良いフィードバックが得られる
  • TODOリストを作ってからコーディングをすることから見積もりや進捗の把握が容易になる
  • TDDはコードの品質を上げ、設計を良くすると言われることが多いが、実装に悪影響を与えやすい構造があると言われることもある
  • TDDは見かけ上の実装コストを引き上げるが、不具合を防止することでトータルのコストを減らすと言われている
  • TDDは自動テストの記述を強制することによって費用対効果の低い自動テストが増え、実装コストや保守コストを過剰に引き上げるという批判がある
    • 特にモックを使用したテストについて言及されることが多い

TDDに関する議論を整理すると、大きな論点としては主に実装コスト、保守コスト、設計の品質の3つがあるようです。よって、これらの論点に対する回答を得るために、関連する話題をまとめてみます。

TDDがソフトウェアの不具合を減らすプロセスについて

TDDがソフトウェアの不具合を減らすということは経験的に知られていることになりますが、論点を明確にするためには因果関係を明確にする必要があります。要因としては主に以下の2つが可能性として考えられます。

  • テストラストの従来の開発方法に比べて自動テストのテストケースの量が増加し、不具合の発生を未然に防いでいる
  • TDDによりコードの品質が向上し、バグを発生させる要因を減らしている

前者に関しては特に異論はないかと思われます。因果関係の説明としては、まずテストファーストであることで、テストケースを網羅することに対する動機を補強し、後回しにしたまま結局やらないという不作為に対する予防効果があります。次に、TDDはコーディングのプロセスの一環として自動テストを記述するため、テストファーストと比較してもテストを書くより強い動機があると言えます。有効な自動テストの量が多くなれば不具合発生率が減ることは自明と言って問題ないでしょう。経験則とも合致しますし、TDDによる不具合発生率に関する研究も行われているため、この話題については概ね正しいとして扱って良いことと思われます。

後者に関しては、詳細は後述しますがTDDの実践によってコードの品質は必ずしも向上するとは限らないことが示唆されています。無論TDDがコードの品質を向上させる可能性はありますが、「うまく活用できれば」という条件付きになりますので議論の際には慎重に取り扱う必要があります。

TDDがソフトウェアの不具合を減らすということに関しては、一般的な話としてはテストケースの増加を主要因として考えるのが良いでしょう。

TDDの種類と混乱

TDDはその理解度や実際にどのように実践しているかによって4つの種類に分けられるという洞察があります。

https://osherove.com/blog/2007/10/8/the-various-meanings-of-tdd.html

TDDが単なるテストファーストのアプローチと混同されがちであることはよく知られていると思いますが、それ以外の類型も存在することが議論をややこしくしている現状があります。

その他にもモックを積極的に使用するかどうか、ユニットテストのみを使うか結合テストも使用するかどうかという分類もあり(後ほど詳細について触れます)、過去の議論を見るに教義が細分化されてしまっていたがために議論に混乱をきたしてしまっていたようです。

TDDに種類があるということとTDDに賛否があるということを総合して考えると、TDDの実践には注意が必要なのではないかという考察が得られます。

「TDD is dead」に始まる一連の話

前提として、「TDD is dead」以前はTDDの悪い面があまり語られないままに、手法に対する理解が甘いまま利用されることも多かったという状況があり、「TDD is dead」はTDDに存在するトレードオフを明確にする意図があったものと考えられます。詳しい経緯などは調べれば見つかるので関連するURLを紹介するだけにとどめて、議論のまとめだけを行うことにします。

http://david.heinemeierhansson.com/2014/tdd-is-dead-long-live-testing.html

https://postd.cc/is-tdd-dead-part1/

https://ubiteku.oinker.me/2015/07/21/tdd-is-dead-long-live-testing/

この一連の議論で大きな話題は主としては以下の2つであるといえます。

  • 自動テストの費用対効果
  • TDDが設計に与える影響

これらについて、順番に触れていきたいと思います。

自動テストの費用対効果について

大きな話題として自動テストの費用対効果についてがありますが、論点が多いです。

  • 自動テストだけで品質を保証できるわけでない
  • 自動テスト自体にビジネス的価値があるわけではない
  • テストカバレッジ100%を目指すことについて
  • TDDによって過剰に自動テストを記述することになる
  • 自動テストには保守工数がかかるということを無視してはいけない

まず、自動テストだけで品質を保証できるわけではないというのはその通りで、目的と状況に合わせて様々な手法を検討して良いでしょう。自動テスト自体にビジネス的価値があるわけではないという話も併せると、ごく短期のプロジェクトなど手動テストで十分に品質を担保しきれる規模であるうちは自動テストの費用対効果が良くないケースもあります。自動テストをほとんど書かないでリリースし、後から自動テストを充実させていくという判断も十分にあり得ることでしょう。Kent Beck 氏もごく短期のプロジェクトでは自動テストの記述を省略しても良い場合があるというようなことを述べており、このようなケースではTDDを採用しない方が良い可能性があります。

https://www.infoq.com/jp/news/2009/06/test-or-not/

テストカバレッジに関しては、100%を目指すと費用対効果が悪くなるというのはよくある話であり、80%から90%くらいが良いとされることが多いです。この辺はやはり費用対効果を考えて柔軟にやっていくのが良いという話が多いです。

https://qiita.com/bremen/items/d02eb38e790b93f44728

TDDによって過剰に自動テストを記述することになるという話についても、Kent Beck 氏は場合によるという回答をしています。TDDによって必ずしも自動テストの量が過剰になるとは限りませんし、最適なテストの量を見極めるのは困難なので、大抵は最適よりも多いか少ないかのどちらかの状態になるはずです。極端に過剰でなければどちらかというと多すぎる方を好むという回答もある程度の納得が得られるものではないでしょうか。

最後に、自動テストには保守工数がかかるという話についてはモックが大きくかかわってきます。モックを使用したユニットテストの実装と保守で苦い思いをしたことがある方は多いと思われます。一般にモックを使用したテストは実装の詳細と密結合するため、実装を変更すると対応するテストの実装を変更する必要があり、モックなどのテストダブルを使用しないテストに比べて実装コストも保守コストも嵩む傾向があります。TDDの批判の多くがこのモックを利用したユニットテストにこだわることに起因していると思われます。これについては「TDD is Fun」の記事が良い回答を示してくれています。

http://diskogs.hatenablog.com/entry/2014/04/25/085112

これによると、必ずしもユニットテストにこだわる必要はなく、結合テストや受け入れテストを使用しても構わないとしています。これによってモック地獄から解放され、自動テストの保守コストが改善する可能性があります。

補足として、結合テストにはいくつかの困難があります。結合テストの仕組みを用意すること、フィクスチャの取り扱い、スローテストの問題などです。TDDを円滑に行うためには結合テストにも一定の書きやすさが必要になりますし、結合テストが自動テストを遅くして開発を妨げることがないようにする必要もあります。特にスローテストの問題は大きいので、一般的な対策をいくつか紹介しておきます。

  • ユニットテストと結合テストを分離し、それぞれを別々に実行できるようにする
  • 結合テストを十分なスペックを持つCIサーバ上で実行する
  • 自動テストを並列化する(結合テストの並列化のためには、例えばDBを使用する場合は並列化された各プロセスが独立したDBにアクセスするなどの対応が必要になることもあるので注意)
  • あまりにも遅くてどうしようもない結合テストについては、仕方ないのでモックを使用することを検討する
  • 結合テストの時だけRDBの代わりにインメモリDBを利用する(完全な互換性があるとは限らないことに留意が必要)

TDDが設計に与える影響について

https://www.infoq.com/jp/news/2017/04/does-tdd-harm-architecture/

この記事によると、TDDが設計に良い設計や悪い設計を生み出すのではなく、TDDを実践する人間がそれらを生み出すのであると結論しています。

しかしながら、TDDには小さなステップを繰り返すサイクルによる開発手法が含まれています。果たしてこれが設計に対して全く影響を与えないということはあるでしょうか。

TDDが設計に与える良い影響としては、テストの記述によってAPIの使い方に関するフィードバックは得られることや、テスト容易性を確保するために疎結合化が促進されることがよく挙げられます。しかし、テスト容易性についての関心が強くなりすぎて抽象化が適切かどうかについての洞察がゆがめられる可能性もあり、結果として過剰な抽象化や間接層を作りこんでしまうこともあり得ます。このように、TDDにはメリットとデメリットの両方があると考えられますが、メリットが強く出る場合とデメリットが強く出る場合があるように思えるため、どちらを重視べきかは場合によると言っていいでしょう。

また、コードのにおいを見落とすとリファクタリングが不十分なままコードが出荷されてしまう可能性があります。要改善なコードを書くことが前提になっている手法であることを踏まえると、経験の浅い開発者がTDDを実践するときにはTDDを行わないときよりも注意を要すると言えなくもありません。ただし、長期的に考えると開発者のトレーニングとして有用である可能性はあります。少なくとも設計が苦手な開発者が行うコーディングという文脈上では、テスト容易性が考慮されたコードはテストが困難なコードよりもマシなことが多い印象があります。密結合だらけでテスト困難なコードに比べれば過剰な抽象化や間接層の問題は比較的些細な問題であり、テストが容易なコードならば多少問題があっても比較的容易にリファクタリングで改善することが出来ることと思います。

まとめると、TDDは設計に良い影響を与える可能性も悪い影響を与える可能性もあります。少なくともTDDを使えば自動的に設計が良くなるということはないので、依然として開発者が設計について深い洞察を持つ重要度は高いと言えます。

TDDに関する研究

TDDがプロジェクトにどのような影響を与えているかに関しては昔から研究対象になっており、論文はTDDの効果について考察するには良い資料になりうるため、いくつか目に留まったものをまとめてみました。

下記の記事にある論文ではTDDによって工数が2割程度増えたと見積もられていますが、不具合密度が9~61%まで低減したとのことなので、少なくとも目に見える実装コストが増える分それなりの恩恵は受けられることがデータから読み取れます。ただし、厳密な比較によって算出された数値ではないため、数値自体の信用度は低いので注意が必要です。

https://blogs.itmedia.co.jp/morisaki/2010/02/tdd-ebd9.html

また、下記の記事にある論文ではTDDによる工数の増加が16%、ブラックボックステストによる不具合検出率が18%減少したことで、定性的な指標としてデバッグ工数の減少とコードの品質の向上を感じる被験者が9割を超えたことが報告されています。

https://blogs.itmedia.co.jp/morisaki/2010/03/tdd---3-5b4a.html

これだけを見ると工数の増加に対して相応の効果を得られていると言っても良いのではないかと思われます。かけたコストに対して適切な効果が得られているかも重要な評価基準になりますが、これを定量的な指標で評価するのは困難です。とりあえずではありますが、TDDが実装コストを増加させると同時に、不具合発生率の指標を改善することで品質の向上に寄与する傾向があることは、経験則とも合致します。

他方、開発者の質やプロジェクトの規模を加味した一般的な傾向についての洞察を深めるために、メタ分析論文も確認してみることとします。幾つかの論文についてまとめいる方がいらっしゃったので、そちらの記事を引用させていただくこととします。

https://yattom.hatenablog.com/entry/20131103/p1

https://yattom.hatenablog.com/entry/20131031/p1

https://yattom.hatenablog.com/entry/20131102/p1

(あまり良いことではないですが)簡単に興味のある傾向だけを抜粋すると、

  • コードの複雑度に関してはTDDを実践した時の方が低い傾向があるように見えるが、はっきりとした結論を出せるほどではない
  • コードの結合度、凝集度に関しては研究によってポジティブな影響が見られる場合とネガティブな影響がみられる場合があり、結論が出ない

といったような傾向があるようです。

このことは、TDDの実践によって自動的にコードの品質が改善するようなことはないということを示唆していると解釈することができます。これは前述の「TDDが設計に与える影響について」で得られた考察と合致します。

GUIのテストについて

例えばGUIのテストに関しては、下記の記事のようにTDDを実践してもあまり恩恵を受けられない場合があるとされることがあります。

https://iansommerville.com/systems-software-and-technology/static/2016/03/17/giving-up-on-test-first-development/

この記事に関してはアンクル・ボブが諦めるには早すぎるとして反論を行っています。これに関しては下記の記事が詳しいです。

https://www.infoq.com/jp/news/2016/04/tdd/

ここからは個人的な意見になりますが、GUIのテストの記述が比較的難しいというのは一般的事実としても問題ないと思われます。それを鑑みると、やはりGUIのテストに関しては費用対効果はやや低めになる傾向がありそうではあり、TDDを使うかどうかはより注意が必要と考えるの無難であるようには思えます。少なくとも現時点ではGUIの実装についてTDDを実践する上での設計上のベストプラクティスもあまり確立されているようには思えないため、GUIにもTDDを適用することを強く勧めるのは難しいのではないかという印象があります。勿論、TDD自体はGUIの実装においても有用である可能性はあり、設計に関する良い洞察が得られるかもしれないので、興味のある方は試してみるのも一興だとは思います。

まとめ

TDDに関する話題をいくつか拾い集めて考察してきましたが、やはりTDDにはメリットだけではなくデメリットもあり、少なくとも銀の弾丸ではないということが読み取れます。厳密に言うとTDDを適用するかどうかは実装単位で費用対効果やメリット・デメリットを検討する必要があると言えます。一部のオブジェクトのみをTDDで作り、他の部分をテストラストで作ることも十分にあり得ます。必ずしもチームメンバー全員がTDDを使用する必要があるわけでもありません。つまり、TDDは開発者一人ひとりの単位で実施できる開発手法、あるいはスキルのひとつと言っても良いでしょう。TDDは柔軟な手法ですので、固定観念にとらわれずに柔軟に活用していきたいところです。

以下に考察で得られた知見をまとめておきます。

大前提

  • TDDは銀の弾丸ではない
  • TDDは単なるテストファーストではなく、RED / GREEN / REFACTOR のサイクルによる設計・実装手法を含む
  • TDDは開発手法のひとつ、あるいはスキルのひとつという理解をするのがベター
  • TDDを適用するかどうかは費用対効果を考えることが重要
  • 開発者ごとに実装単位でTDDを利用して実装するかを選択して良いので、メリットデメリットを理解したうえで、TDDを道具として柔軟に使えばよい
    • 一部のオブジェクトのみをTDDで作り、他の部分をテストラストで作ることも十分にあり得る

TDDによって得られる可能性があるメリット

  • 十分な量の自動テストが存在する状態になり、以下のような恩恵を受けることができる
    • エラーの発見や防止がしやすくなる → プロダクトの品質の指標の一つである不具合発生率の低下に貢献する
    • リファクタリングのリスクを減らし、リファクタリングを行う意思決定の助けになる
    • 充実した自動テストがAPIのドキュメント代わりになる
  • 大きな機能や複雑な機能に対しても小さなステップを一つ一つ着実にこなしていくという点で完成までの道筋が明確になり、自信をもってコードを書き進められるようになる
    • 実装のしやすさにもつながり、時には開発速度の向上に寄与することもある
  • テストの記述を通して抽象化に関する良いフィードバックが得られる
  • TODOリストを作ってからコーディングをすることから見積もりや進捗の把握が容易になる
  • TDDはテスト容易性の観点からコードの品質を上げ、設計を良くする可能性がある
  • TDDは見かけ上の実装コストを引き上げるが、不具合を防止することでトータルのコストを減らす可能性がある

TDDの実践にあたっての留意事項

  • TDDによる利点の多くはTDD以外の方法でも部分的に実現することが可能なので、TDDが唯一の良い方法というわけではない
    • TDDのデメリットが気になる場合で、単なるテストファーストで十分なのであればテストファーストのみを採用すればよい
  • TDDやテストファーストにはプロジェクトによって適さない場合が存在する可能性があり、以下のようなケースでは注意を要する(全く適していないという意味ではないので都度判断が必要)
    • ごく短期のプロジェクト
    • コードの規模がごく小さいプロジェクト
    • すぐに実装が捨てられる可能性が高いとき、例えばプロジェクト全体の仕様がはっきりと定っていないとき、実験的な性質が強い機能の実装のとき
    • 開発者の経験が浅く、設計についてのノウハウが不足しているとき
  • TDDを実践する際は、開発者が設計についてある程度熟知していなければ十分な効果を得にくく、TDDによって必ずしもソフトウェアの設計が良くなるとは限らない
    • TDDが設計に与える影響は良い面だけでなく悪い面も存在する可能性があり、開発者の熟練度による影響が大きい
    • 短期的には経験の浅い開発者にとってTDDは有用ではない可能性がある
    • 長期的に見るとTDDの実践が経験の浅い開発者のトレーニングとして有効である可能性はある
  • 経験則として、TDDを適用する対象の実装の性質によって向き不向きがある
    • GUIなどのフロントエンド実装や外部システムとの結合度が高い実装では思ったほどの効果が得られない可能性がある
    • (これは全く向いていないというわけではなく、何のためにTDDを使用するかを意識して使う必要性がより高くなるという話)
  • TDDを実践するときはユニットテストにこだわらず、モックの利用は最低限に抑え、結合テストや受け入れテストも活用していく方が恩恵をより受けやすくなる
    • 逆に、モックを大量に使うことをある程度抑制しないと、実装コストと保守コストがかさみ、実装の変更を阻害し、自動テストの費用対効果が悪くなる可能性がある

Discussion