新しいソフトウェアプロジェクトを設計する際、どのように構造を決定するかについて多くの選択肢に直面することがよくあります。コアの抽象化は何であるべきか?それらはどのように相互作用すべきか?
この投稿では、これらの質問に答えたり影響を与えたりするための有用なガイドとして私が見つけた設計のヒューリスティックを提案したいと思います:
コードをテストしやすく最適化せよ
具体的には、新しいコードを書くとき、それを設計し、システムの他の部分との関係を設計する際に、自問自答してください。この質問です:「このコードをどのようにテストしますか?環境やシステムの他の部分について最小限の無関係な仮定で、このコードの正確性を確認する自動テストをどのように書きますか?」そして、その質問に対する良い答えがない場合は、抽象化やインターフェースを再設計してください。
私はこのヒューリスティックが 2 つの異なる方法で価値があると感じており、ここで両方について説明します。
良いテストが得られる#
これはおそらく明白です:良いテストを持つことを目標にすると、良いテストを得る可能性が高くなります。
しかし、もう少し詳しく述べる価値があると思います。
まず、良いテストを持つコードベースで作業することがどれほど素晴らしいかを強調したいです。変更を検証するプロセスはシンプルです:テストを実行するだけです。複雑な開発環境を設定したり、システムを手動または対話的に操作したりする必要はありません。単に make test
を実行すれば、保証はないにしても、あなたのコードが思っている通りに動作し、重要なものを壊していないという高い信頼を得ることができます。
大規模で成熟したソフトウェアシステムでは、変更を加える際の最も難しい部分は変更そのものではなく、他の重要な動作を後退させない方法で変更を加えることです。したがって、基本的な保証を迅速に提供できる良い、迅速なテストは、非常に大きな生産性の向上をもたらします。
次に、良いテストの利点を本当に実現するためには、テストを迅速に実行できる必要があります。つまり、変更をカバーするテストのサブセットを迅速に実行できる必要があります。これにより、迅速な開発サイクルを維持できます。
この特性をシステムがスケールする際に保持するためには、良い ユニット テストが必要です。「ユニット」、「機能」、「統合」などのテストの境界については多くの宗教的な議論がありますが、ここでの「ユニット」テストとは、アプリケーション内の他のコードへの依存関係や呼び出しを最小限に抑えた特定のモジュールやコンポーネントをテストすることを意味します。
また、アプリケーションをエンドツーエンドでテストするための統合テストも必要ですが、主にユニットテストに依存することには重要な利点があります:
- ユニットテストは速い:限られた量のコードをテストすることで、アプリケーション全体を呼び出す必要があるテストよりも迅速に実行されます。
- さらに重要なのは、ユニットテストはスケールします:主にユニットテストで構成されたコードベースでは、テストの速度はアプリケーションのサイズに対して線形にスケールするはずです。機能的なエンドツーエンドテストが増えると、各コンポーネントが他のほぼすべてのコンポーネントを偶然にテストする必要があるため、スケールが二次的に近づくリスクがあります。
- ユニットテストは明確なコードの部分に関連付けられているため、特定の変更に対応するテストのみを実行するのが容易で、開発へのフィードバックループがさらに迅速になります。
しかし、良いユニットテストは偶然には発生しません。良い「ユニット」がなければ、良いユニットテストを持つことは非常に難しいです。狭く明確に定義されたインターフェースを持つコードの部分は、それらのインターフェースでテストできます。
そのような論理的なユニットを持ち、優れたテストを可能にし、迅速なフィードバックサイクルを維持しながら成熟したプロジェクトを維持する最良の方法は、直接それを行うことです:この目的を念頭に置いてコードを書くことです。
より良いコードが得られる#
テストを念頭に置いてコードを書く第二の理由は、実際により良いコードが得られることです!
テストしやすいコードが持つべき特性をいくつか考えてみましょう:
不変データよりも純粋な関数を好む#
不変データ構造よりも純粋な関数は、テストが 非常に 簡単です:単に(例の入力、期待される出力)のペアのテーブルを作成できます。また、入力が簡単に説明できるため、QuickCheck のようなツールでファジングするのも一般的に簡単です。
明確に定義されたインターフェースを持つ小さなモジュール#
コードの一部が小さく、明確に定義されたインターフェースを持っている場合、そのインターフェースに基づいてブラックボックステストを書くことができ、モジュールの内部やシステムの他の部分について過度に気にすることなく、記述されたインターフェース契約をテストできます。
IO と計算の分離#
一般的に、IO は純粋なコードよりもテストが難しいです。テスト可能なシステムは、純粋なロジックから IO を隔離し、別々にテストできるようにします。
依存関係の明示的な宣言#
グローバル環境からデータベース名を暗黙的に取得するコードは、ハンドルを引数として受け取るコードよりもテストが難しいです。後者は、テストの過程で異なるハンドルを使用して呼び出すことができ、毎回クリーンなデータベースに対してテストしたり、複数のスレッドで同時にテストしたり、必要なことを行ったりできます。
一般的に、テスト可能なコードは、暗黙的な可用性を仮定するのではなく、どこかで依存関係を明示的な引数として受け入れます。
これらの特性を見ると、それらは一般的に 良い、構造化されたコード の特性でもあることがわかります!テストを念頭に置いてシステムを設計することで、通常はそうでなかった場合よりも良くファクタリングされたシステムを得ることができます。
さらに、これらの特性に間接的にアプローチすることで、テストの観点からそれらをより具体的にします。「このコードを適切にテストしやすくするためには何が必要か?」という具体的な枠組みに直面すると、選択肢を評価し、どこまで進んだかを決定するのが容易になります。
結論#
複雑なソフトウェアシステムにとって、信頼を持って変更を加える能力ほど重要なものは少なく、高品質の自動テストはこの目的のために私たちが持つ最も重要なツールの 1 つです。良いテストは偶然には発生せず、単なる力任せの努力によっても生じません。それらは設計によって生じます。なぜなら、アプリケーションがそれらを可能にするように書かれているからです。
ソフトウェアを書くときは、常に「このソフトウェアの正確性をどのようにテストしますか?」と自問し、その目標に向けて設計することを厭わないでください。その結果、あなたはより自信を持てるシステムを得ることができ、他の面でもより良く構造化されたものになります。