ユニットテストとTDD:有用か、過大評価か?現場のエンジニアの本音

ユニットテストとTDD:有用か、過大評価か?現場のエンジニアの本音 プログラミング
テストファースト原理主義は、禁欲のみの性教育に似ている。
非現実的で効果のない道徳キャンペーンだ

この刺激的な言葉を読んだとき、あなたはどう感じるでしょうか。

海外の開発者コミュニティでは、ユニットテストやTDD(テスト駆動開発)について驚くほど多様な意見が飛び交っています。DHHやCasey Muratoriといった著名なプログラマーたちは、集中的なTDD/ユニットテストアプローチに強く批判的です。

本記事では、海外の開発者コミュニティで議論されている内容をもとに、ユニットテストとTDDの実践的な価値について考察します。

TDD批判派の主張

批判的な立場を取るプログラマーたちは、主に次のような問題点を指摘しています。

まず、モックテストは偽りの安心感を与えてしまうという点。
次に、コードがテスト可能になるように書かれることで、逆に複雑になるという点。

さらに、リファクタリング中にテストが頻繁に壊れるという点。
そして、エンドツーエンドのシステムテストを置き換えることはできないという点です。

長年コードを書いてきた開発者の中には、こう感じる人もいます。
「テストを書くことは、プログラミングの実質的な助けというより、官僚的な義務に感じる」と。

では、これらの批判は正当なのでしょうか。

「テストしづらいコードは、保守しづらい」

あるベテラン開発者は興味深い視点を提示しています。

「私の観点からすると、コードはテスト可能でなければならないからこそ、むしろシンプルになる」

テストの複雑さが増したとします。
そして、ビジネスロジックの単純な変更がテストの大幅な修正を必要とする。
それは本番コードのリファクタリングと適切な分離が必要だというシグナルだ、と彼は続けます。

別の開発者はこれを簡潔にまとめました。
「テストしづらいなら、保守しづらい」
この言葉は、テストとコード品質の関係を端的に表現しています。

純粋関数というアプローチ

テストのしやすさという観点では、純粋関数が最も優れているという意見も出ています。

隠れた状態への依存を避ける。
I/Oとロジックの混在を避ける。

そうすることで、ユニットテストは驚くほど簡単でシンプルになります。
モックオブジェクトさえ必要ありません。

逆に「汚い」コードを書くとどうなるか。
ユニットテストは高コストになります。

モックオブジェクトの作成が必要になる。
グローバル状態の設定も必要になる。

そして最も難しいのは、テスト内のグローバル状態が正しいことを確認する作業です。
つまり、ユニットテストのコストはテストそのものではなく、テスト対象のコードの設計に大きく依存するのです。

100%カバレッジは不要

テックリードの立場から発言した開発者は、こう述べています。

脆弱な部分と重要な部分をテストする。
100%カバレッジは必要ない。
必要なのは、重要なことを確認するスマートなカバレッジだ

ユニットテストは目的を達成するためのツールの一つに過ぎません。
エンドツーエンドテストも別のツールです。
おそらく、両方が少しずつ必要になるでしょう。

テストが必要な場面を考えてみましょう。

大規模なリファクタリング後にアプリが動作するかどうかの確認。
新機能追加後の動作確認。
重要なロジックが正しく機能しているかのチェック。

これらの目的に対して、適切なテスト戦略を選ぶことが重要です。

ほとんどのページでHTTP 200が返ってくるかだけを確認すれば十分な場合もあります。
しかし、重要な部分にはしっかり時間を投資してください。
さもなければ、後で後悔することになるでしょう。

テストファーストの是非

「コードを書く前にテストを書く」というTDDの基本原則についても、意見は分かれています。

ある開発者はこう主張します。
「存在しないものに対して意味のあるテストを書くことはできない」と。

設計が進化するにつれてテストを書き直し続けることになる。
動く標的をテストしているようなものだ、と。

一方で、別の開発者は反論します。
「テストを書くことで、システムの設計について考えることを強制される」と。
テストファーストと実装ファーストの間には、相互作用があるというのです。

最終的には、自分に合った方法を見つけることが重要だという点で、多くの開発者は一致しています。

ある開発者はこう述べました。

プログラミングするとき、事前計画はほとんど行わない。
頭の中に大まかな設計はある。
しかし、詳細にスケッチすることは稀だ。
ソフトウェアは進めながら有機的に進化する。
だから、事前にテストを書くのは私には非現実的だ

リファクタリング中にテストが壊れる問題

「リファクタリング中にテストが頻繁に壊れる」という批判について、ある開発者は痛烈に反論しています。

リファクタリング中にテストが壊れることに文句を言うのは、高電圧時にヒューズが飛ぶことに文句を言うようなものだ

テストはコードが何をするかという契約を記述する助けになります。
リファクタリング中にテストケースを数行読んだとしましょう。

その時点で、テストはすでに有用だったのです。
そうでなければ、コードが何をしているか理解しようとする時間を費やすことになるからです。

ただし、ここには重要な区別があります。
適切にコンポーネントを分離し、コンポーネントの契約を適切に定義していれば、テストは壊れません。
テストが頻繁に壊れるなら、それはコードの結合度が高すぎるというシグナルかもしれないのです。

エンドツーエンドテストとの関係

「ユニットテストはエンドツーエンドシステムテストを置き換えない」という批判に対して、多くの開発者は「それは当然だ」と応えています。

あるベテラン開発者は独自の視点を提示しました。

テストピラミッドは、自動テストが始まった当時の制限の結果に過ぎない。
唯一有用なテストはエンドツーエンドテストだ。
なぜなら、コードをリリースするのではなく、機能をリリースするのだから。
ユーザーはアプリの内部のゴタゴタなど気にしない。
猫を見るボタンをクリックしたら猫を見たいのだ

しかし、別の開発者は反論します。

ユニットテストは、なぜユーザーが猫を見られないのかを絞り込むためのものだ。
通常、猫とは関係ないことが原因だからね

この二つの視点は、異なるテストタイプが異なる目的を持つことを示しています。

時間とリソースのトレードオフ

現実的な視点として、時間とリソースの問題を指摘する声もあります。

テストは重要だ。小さな変更が何かを壊さないことを保証する助けになる。
自動化できる別のテストレイヤーだ。
しかし、テストを書くことは時間がかかる。
コーディングやデバッグと同様に。
だから、独自の時間を割り当てるべきだ

上層部はしばしばテストの重要性を強調します。
自分たちの安心のためでしょう。

しかし、現実的な見積もりが提示されるとどうなるか。
優先順位はすぐに下げられるか、オプション扱いになります。

結局のところ、時間と優先順位の問題です。
素早くリリースすることが目標なら、完全なTDDや広範なユニットテストは現実的ではないかもしれません。

ただし、エンドツーエンドテストは依然として行うべきでしょう。
十分な時間とリソースがあれば、TDDとユニットテストは信頼性と長期的な保守性を向上させる価値ある投資になります。

ドグマを捨てる

20年のソフトウェア開発経験を持つ開発者は、興味深い観察を共有しています。

純粋なTDD(すべてのテストをコードの前に書く)を使用するチームで働いたことは一度もない。
ブログ記事やカンファレンストークの外では、実際のPHPプロジェクトでは極めて稀だ

彼はまた、「複雑さ」という言葉の解釈についても言及しています。
多くの場合、人々が不満を言う「複雑さ」は、単なる依存性注入、小さな関数、明確な境界に過ぎない。

それは複雑さではない。
基本的なソフトウェア設計だ、と。

ある開発者は明快にまとめました。

TDDは有用だが、場合による。
ドグマを捨てて、意味があるときに意味のあることを使うべきだ。
『TDDは決して有用ではない』も『常にTDDを使え』も同様にドグマ的だ

まとめ

ユニットテストとTDDの価値は、状況によって大きく変わります。

テストは、PRがマージ可能になるまでに存在すべきです。
しかし、いつ作成するかについて宗教的になる必要はありません。

重要なのは、テストしやすいコードを書くこと。
そして、それは多くの場合、保守しやすいコードでもあります。

純粋関数を活用する。
依存関係を明確にする。
ロジックとI/Oを分離する。
そうすることで、テストは驚くほど簡単になります。

100%カバレッジを目指すのではなく、重要な部分を賢くテストする。
ユニットテストとエンドツーエンドテストは異なる目的を持つことを理解し、両方を適切に使い分ける。

結局のところ、テストは道具です。
道具は、正しく使えば価値がある。

間違って使えば害になります。
ドグマに従うのではなく、プロジェクトの特性と自分のワークフローに合わせて、最適なアプローチを見つけることが大切でしょう。

タイトルとURLをコピーしました