web-dev-qa-db-ja.com

単体テストは設計をどのように促進しますか?

私たちの同僚は、実際に私たちが私たちのデザインを改良し、物事をリファクタリングするのを助けるものとしてユニットテストを書くことを奨励していますが、私はどのようにしているかわかりません。 CSVファイルを読み込んで解析する場合、単体テスト(フィールドの値の検証)で設計を検証するにはどうすればよいですか?彼はカップリングやモジュール性などに言及しましたが、私にはあまり意味がありません-しかし、私には理論的な背景はあまりありません。

これは、重複としてマークした質問と同じではありません。「役立つ」という理論だけでなく、これがどのように役立つかを実際の例に興味があります。以下の回答とコメントが好きですが、もっと詳しく知りたいです。

43
User039402

単体テストは設計を容易にするだけでなく、それが主要な利点の1つです。

テストファーストを作成すると、モジュール性とクリーンなコード構造が実現します。

コードをテストファーストで作成すると、特定のコードユニットの「条件」は、コードでそれらを想定すると、自然に依存関係(通常はモックまたはスタブを介して)にプッシュされます。

「与えられた条件x、期待される動作y」は、テストを行うx(これはシナリオである)を提供するスタブになることがよくあります。現在のコンポーネントの動作を確認する必要があります)とyはモックになり、呼び出しはテストの最後に確認されます(「yを返す必要がある」の場合を除く)。その場合、テストは戻り値を明示的に検証するだけです)。

次に、このユニットが指定どおりに動作したら、検出した依存関係(xおよびyの)の記述に進みます。

これにより、クリーンでモジュール化されたコードの記述が非常に簡単で自然なプロセスになります。

後でテストを作成すると、コードの構造が不十分であることがわかります。

スタブやモックするものが多すぎるため、または相互に密結合しすぎているためにコードのテストを書くことが困難になった場合、コードに改善を加える必要があることがわかります。

単一のユニットに非常に多くの動作があるために「テストの変更」が負担になる場合、コード(または単にテストを書くためのアプローチ)に改善を加える必要があることがわかりますが、これは通常私の経験では当てはまりません) 。

さらに抽象化する必要があるために、シナリオが複雑になりすぎる場合( "if x and y and z then ...")、さらに抽象化する必要があるため、あなたのコード。

重複と冗長性のために、2つの異なるフィクスチャで同じテストが行​​われる場合、コードに改善を加える必要があることがわかります。

これはMichael Feathersによる優れた講演です コード内のテスト容易性と設計の間の非常に密接な関係を示しています(元はコメントのdisplayNameによって投稿されていました)。トークはまた、一般的に良いデザインとテスト容易性に関するいくつかの一般的な不満と誤解に対処します。

3
Ant P

ユニットテストの優れた点は、他のプログラマがコードを使用する方法と同じようにコードを使用できることです。

あなたのコードがユニットテストにぎこちない場合、それはおそらく使いにくいでしょう。フープを飛ばさずに依存関係を注入できない場合、コードはおそらく柔軟に使用できなくなります。また、データの設定や実行する順序の決定に多くの時間を費やす必要がある場合、テスト中のコードはおそらく結合が多すぎて、作業が面倒になります。

103
Telastyn

理解するのにかなり時間がかかりましたが、テスト駆動開発(singユニットテスト)を行うことの本当の利点(編集:私にとって、あなたの走行距離は異なる場合があります)は事前にAPI設計を行う

開発への典型的なアプローチは、与えられた問題を解決する方法を最初に理解することであり、その知識と初期実装設計を使用して、ソリューションを呼び出すための何らかの方法です。これにより、かなり興味深い結果が得られる場合があります。

TDDを行うときは、最初にseとなるコードを記述する必要があります。入力パラメーター、および予想される出力。これにより、正しいことを確認できます。そのためには、実際に何をする必要があるかを理解する必要があるため、意味のあるテストを作成できます。そのときだけ、ソリューションを実装します。あなたのコードが何を達成することになっているのかを正確に知っているとき、それはより明確になるというのも私の経験です。

次に、実装後の単体テストは、リファクタリングによって機能が損なわれないことを確認し、コードの使用方法に関するドキュメントを提供します(テストに合格したことはわかっています)。しかし、これらは二次的なものです。最大のメリットは、最初にコードを作成する際の考え方です。

単体テストは「設計を改善し、物事をリファクタリングするのに役立つ」と100%同意します。

それらが初期設計を実行するのに役立つかどうかについて、私は2つの考えを持っています。はい、それらは明らかな欠陥を明らかにし、「どうすればコードをテスト可能にすることができるのか」について考えることを強制しますか?これにより、副作用が少なくなり、構成とセットアップが容易になります。

ただし、私の経験では、設計がどうあるべきかを実際に理解する前に書かれた過度に単純化された単体テスト(確かに、それはハードコアTDDの誇張ですが、多くの場合、プログラマーが多くのことを考える前にテストを書く)は貧血につながります内部モデルが多すぎるドメインモデル。

TDDでの私の経験は数年前だったので、基礎となる設計に過度のバイアスをかけないテストを作成するのに役立つ新しいテクニックを聞いてみたいと思います。ありがとう。

6
user949300

ユニットテストを使用すると、関数間のインターフェイスがどのように機能するかを確認でき、ローカルデザインと全体的なデザインの両方を改善する方法に関する洞察を得ることができます。さらに、コードの開発中に単体テストを開発すると、既製の回帰テストスイートが作成されます。 UIを開発しているのか、バックエンドライブラリを開発しているのかは関係ありません。

プログラムが(単体テストを使用して)開発されると、バグが明らかになるので、テストを追加してバグが修正されたことを確認できます。

一部のプロジェクトではTDDを使用しています。私は、教科書や正しいと考えられる論文から引き出す例を作成することに多大な努力を払い、これらの例を使用して開発しているコードをテストします。方法に関して私が持っている誤解は非常に明白になります。

コードが最初に記述されているか、テストが最初に記述されているかは気にしないので、私は一部の同僚より少し緩い傾向があります。

5
Robert Baron

パーサー検出値の区切りを適切にユニットテストする場合は、CSVファイルから1行渡してください。テストを直接かつ簡潔にするために、1行を受け入れる1つの方法でテストすることができます。

これにより、行の読み取りと個々の値の読み取りを自動的に分離できます。

別のレベルでは、すべての種類の物理CSVファイルをテストプロジェクトに配置するのではなく、読みやすさを向上させるために、テスト内で大きなCSV文字列を宣言して、読みやすさとテストの目的を向上させます。これにより、パーサーを他の場所で行うI/Oから切り離すことができます。

ほんの基本的な例です。練習を始めてください。いつか魔法を感じるでしょう(私が持っています)。

5
Joppe

簡単に言えば、ユニットテストを記述すると、コードの欠陥が明らかになります。

Jonathan Wolter、Russ Ruffer、MiškoHeveryによって作成されたこのすばらしい テスト可能なコードを書くためのガイド には、コードの欠陥がテストを阻害し、簡単に再利用できず、同じコード。したがって、コードがテスト可能であれば、より使いやすくなります。 「モラル」のほとんどは、コード設計を大幅に改善する途方もなく単純なヒントです( 依存性注入 FTW)。

例:キャッシュが要素の排除を開始したときに、computeStuffメソッドが適切に動作するかどうかをテストすることは非常に困難です。これは、「bigCache」がほぼいっぱいになるまで手動でクラップをキャッシュに追加する必要があるためです。

_public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}
_

ただし、依存性注入を使用すると、キャッシュが要素の排除を開始したときに、computeStuffメソッドが適切に動作するかどうかをテストする方がはるかに簡単です。行うのは、new HereIUseDI(buildSmallCache());を呼び出すテストを作成することだけです。通知には、オブジェクトのより微妙な制御があり、すぐに配当が支払われます。

_public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}
_

私たちのコードが通常データベースに保持されているデータを必要とする場合にも同様の利点があります...必要なデータを正確に渡すだけです。

4
Ivan

「ユニットテスト」の意味によっては、実際に低レベルとは思わない(== --- ==)ユニットテストは、わずかに高いレベルintegrationテスト-コード内のアクター(クラス、関数など)のグループが適切に結合して、一連の望ましい動作を生成することをテストするテスト開発チームと製品所有者の間で合意されました。

これらのレベルでテストを記述できる場合、多くのクレイジーな依存関係を必要としない、ニースで論理的なAPIのようなコードを作成するように促されます-単純なテスト設定をしたいという欲求は、当然のことながら多くのものを持たないようにしますクレイジーな依存関係または密結合コード。

ただし、間違いはありません-ユニットテストは、良いデザインだけでなく、悪いデザインにもつながる可能性があります。開発者が、Nice論理設計と単一の懸念をすでに持っている少しのコードを取り、それを分離して、純粋にテストの目的でより多くのインターフェースを導入し、その結果、コードの可読性を低下させ、変更を困難にするのを見てきました。 、さらに、開発者が低レベルの単体テストをたくさん持つことで、より高レベルのテストを行う必要がないことを決定した場合は、バグが増える可能性もあります。特にお気に入りの例は、クリップボードの情報の取得と取得に関連する、非常に細分化された「テスト可能な」コードがたくさんあったバグを修正したことです。すべてが細かく分解され、非常に小さな詳細レベルに分離されています。多くのインターフェース、テストのモック、その他の楽しいものがあります。問題は1つだけです。OSのクリップボードメカニズムと実際にやり取りするコードがなかったため、本番環境のコードは実際には何もしませんでした。

ユニットテストは間違いなくあなたのデザインを動かすことができます-しかし、それらは自動的にあなたを良いデザインに導くわけではありません。あなたは単に「このコードがテストされるだけでなく、良いデザインが何であるかについてのアイデアを持つ必要があります。それはテスト可能なので、それは良いことです。

もちろん、「単体テスト」が「UIを介して駆動されないすべての自動テスト」を意味する人々の1人である場合、それらの警告の一部はそれほど関連性がない可能性があります。レベルの統合テストは、多くの場合、設計を推進する上でより有用なものです。