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


43

私たちの同僚は、ユニットテストの作成を、実際に設計を改良し、リファクタリングするのに役立つものとして推進していますが、その方法はわかりません。CSVファイルを読み込んで解析する場合、ユニットテスト(フィールドの値の検証)は設計の検証にどのように役立ちますか?彼は結合やモジュール性などに言及しましたが、私にはあまり意味がありません-しかし、私は理論的な背景があまりありません。

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



3
以下の答えは本当にあなたが知る必要があるすべてです。集約ルート依存関係を注入したファクトリーファクトリーファクトリーを1日中記述する人々の隣に座っているのは、正しく機能し、検証しやすく、既に文書化されている単体テストに対して単純なコードを静かに書く人です。
ロバートハーベイ

4
自動的にTDDを意味するものではありませんユニットテストをやって@gnat、それは別の問題だ
Joppe

11
「単体テスト(フィールドの値を検証する)」 -単体テストと入力検証を混同しているようです。
jonrsharpe

1
@jonrsharpe CSVファイルを解析するコードであることを考えると、彼は特定のCSV文字列が期待される出力を提供することを検証する実際の単体テストについて話しているかもしれません。
JollyJoker

回答:


3

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

テストファーストを書くことは、モジュール性とクリーンなコード構造を引き出します。

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

「与えられた条件x、振る舞いyを期待する」は、しばしばスタブになりx(これは、テストが現在のコンポーネントの振る舞いを検証する必要があるシナリオです)y、モックになります。テストの終わり(「should return」でないy場合)。この場合、テストは戻り値を明示的に検証するだけです。

次に、このユニットが指定されたとおりに動作したら、発見した依存関係(xy)の記述に進みます。

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

後でテストを書くと、コードの構造が不十分なときにわかります。

スタブやモックすることが多すぎるため、または物同士が密に結合されているために、コードの一部のテストを作成することが困難になる場合、コードを改善する必要があることがわかります。

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

あなたのシナリオは、(「もしあまりにも複雑になった場合xyし、zあなたが抽象それ以上に必要があるため、あなたはあなたのコードで作る改善を持って知って、その後...」)。

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

以下は、Michael Feathersによる優れた講演で、テスト可能性とコードのデザインとの非常に密接な関係を示しています(元々はコメントでdisplayNameによって投稿されました)。この講演では、一般的な優れた設計とテスト容易性に関する一般的な苦情と誤解についても取り上げています。


@SSECommunity:今日の時点でたった2つの賛成票で、この答えは見落としがちです。この回答にリンクされているMichael Feathersの講演を強くお勧めします。
displayName

103

単体テストの素晴らしいところは、他のプログラマがあなたのコードを使用する方法でコードを使用できることです。

あなたのコードがユニットテストで扱いにくい場合、おそらく使いにくいでしょう。フープを介さずに依存関係を注入できない場合、コードはおそらく柔軟性に欠けるでしょう。また、データのセットアップや処理の順序の決定に多くの時間を費やす必要がある場合、テスト対象のコードのカップリングが多すぎるため、作業が面倒になります。


7
素晴らしい答え。私は常に、テストをコードの最初のクライアントと考えています。テストを書くのが苦痛なら、APIや私が開発しているものを消費するコードを書くのは痛いでしょう。
スティーブンバーン

41
私の経験では、ほとんどの単体テスト「他のプログラマーがコードを使用する方法でコードを使用する」ことはありません単体テストがコードを使用するので、彼らはあなたのコードを使用します。確かに、彼らは多くの深刻な欠陥を明らかにします。ただし、単体テスト用に設計されたAPIは、一般的な使用に最適なAPIではない場合があります。単純に記述された単体テストでは、多くの場合、基礎となるコードが多くの内部を公開する必要があります。繰り返しますが、私の経験に基づいて-これをどのように処理したかを聞いてみたいと思います。(以下の私の答えを参照)
user949300

7
@ user949300-私は最初にテストを大事にする人ではありません。私の答えは、最初にコード(そして確かにデザイン)のアイデアに基づいています。APIは単体テスト用に設計されるべきではなく、顧客向けに設計されるべきです。単体テストは顧客の概算に役立ちますが、ツールです。彼らはあなたに奉仕するためにそこにいます。その逆ではありません。そして、彼らは確かにあなたがくだらないコードを作るのを止めるつもりはありません。
テラスティン

3
私の経験における単体テストの最大の問題は、最初から良いコードを書くのと同じくらい難しいことです。良いコードと悪いコードを区別できない場合、単体テストを作成してもコードが改善されることはありません。単体テストを作成するときは、スムーズで快適な使用法と「厄介な」または難しい使用法を区別できる必要があります。彼らはあなたにあなたのコードを少し使用させるかもしれませんが、彼らはあなたがしていることが悪いことをあなたに強制的に認識させません。
jpmc26

2
@ user949300-ここで念頭に置いた古典的な例は、connStringを必要とするリポジトリです。それを書き込み可能なパブリックプロパティとして公開し、リポジトリをnew()した後に設定する必要があるとします。5回目または6回目以降は、そのステップを実行することを忘れたテストを作成し、それによってクラッシュします。connStringをクラス不変に強制することに「自然」に傾くので、コンストラクターに渡されます。 APIを改善し、このトラップを回避する生産コードを記述できるようにします。保証ではありませんが、助けになります。
スティーブンバーン

31

実現するのにかなり時間がかかりましたが、テスト駆動開発(ユニットテストを使用)を行うことの本当のメリット(編集:私にとって、あなたのマイレージは異なる場合があります)は、APIデザインを事前に行う必要があることです

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

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

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


それは確かに利点ですが、私はそれが「本当の」利点だとは思いません。本当の利点は、コードのテストを書くと自然に「条件」が依存関係に押し出され、依存関係の過剰注入を打ち消すことです)開始する前。
Ant P

問題は、そのAPIに一致するテストセット全体を事前に作成すると、必要に応じて正確に機能せず、コードとすべてのテストを書き直す必要があることです。一般向けAPIについては、変更されない可能性が高く、このアプローチは問題ありません。しかし、内部でのみ使用されるコードのためのAPIを使用すると、一緒に仕事セミプライベートのAPIの多くを必要とする機能を実装する方法を見つけ出すように多くを変更し
フアン・メンデス

@AntPはい、これはAPI設計の一部です。
するThorbjörnRavnアンデルセン

@JuanMendesこれは珍しいことではなく、要件を変更するときの他のコードと同様に、これらのテストを変更する必要があります。優れたIDEは、メソッドシグネチャなどを変更したときに自動的に行われる作業の一部としてクラスをリファクタリングするのに役立ちます。
ThorbjørnRavn Andersen

@JuanMendes優れたテストと小さなユニットを書いている場合、説明している効果の影響は実際にはほとんどありません。
アリP

6

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

彼らがあなたが初期設計をするのを助けるかどうかについて私は2つの心です。はい、それらは明らかな欠陥を明らかにし、「コードをテスト可能にする方法」について考えるように強制しますか?これにより、副作用が少なくなり、構成とセットアップが容易になります。

しかし、私の経験では、設計がどうあるべきかを実際に理解する前に書かれた過度に単純化された単体テスト(確かに、それはハードコアTDDの誇張ですが、あまりにも頻繁にコーダーが考える前にテストを書く)はしばしば貧血につながりますあまりにも多くの内部を公開するドメインモデル。

TDDでの私の経験は数年前でしたので、基礎となる設計にあまり偏りのないテストを書くのに役立つ新しい技術を聞くことに興味があります。ありがとう。


多数のメソッドパラメータは、コードのにおいと設計上の欠陥です。
スーフィアン

5

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

プログラムが(ユニットテストを使用して)開発されると、バグが発見されたため、テストを追加してバグが修正されたことを確認できます。

私は自分のプロジェクトのいくつかにTDDを使用しています。教科書や正しいと思われる論文から引用したサンプルを作成し、これらの例を使用して開発中のコードをテストします。方法に関して私が持っている誤解は非常に明白になります。

コードを先に書くかテストを先に書くかは気にしないので、私は同僚の何人かより少しゆるい傾向があります。


それは私にとって素晴らしい答えです。いくつかの例を挙げてください。たとえば、各ケースに1つずつ(設計などの洞察を得るとき)。
User039402

5

適切に区切られた値を検出するパーサーを単体テストする場合、CSVファイルから1行渡すことをお勧めします。テストを直接短くするために、1行を受け入れる1つのメソッドでテストすることをお勧めします。

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

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

ほんの基本的な例です。練習を始めると、ある時点で魔法を感じるでしょう(私は持っています)。


4

簡単に言えば、単体テストを作成すると、コードの欠陥が明らかになります。

Jonathan Wolter、Russ Ruffer、およびMiškoHeveryによって書かれた、テスト可能なコードを記述するためのこの壮大なガイドには、テストを阻害し、同じコードの容易な再利用と柔軟性を妨げるコードの欠陥の例が数多く含まれています。したがって、コードがテスト可能であれば、使いやすくなります。「モラル」のほとんどは、コードデザインを大幅に改善するとんでもないほど単純なヒントです(Dependency Injection 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?
   }
}

通常、データベースに保持されているデータがコードで必要な場合にも、同様の利点が得られます。必要なデータを正確に渡すだけです。


2
正直なところ、この例をどういう意味なのかわかりません。メソッドcomputeStuffはキャッシュとどのように関連していますか?
ジョンV

1
@ user970696-はい、「computeStuff()」がキャッシュを使用することを暗示しています。問題は、「常にcomputeStuff()が正しく動作するかどうか(キャッシュの状態によって異なります)」です。そのため、直接設定できない場合、computeStuff()がすべての可能なキャッシュ状態に対して希望どおりに動作することを確認するのは困難です。 「cacheOfExpensiveComputation = buildBigCache();」という行をハードコーディングしたため、キャッシュを構築します (コンストラクターを介して直接キャッシュに渡すのとは対照的に)
イヴァン

0

「ユニットテスト」の意味に応じて、実際には低レベルのユニットテストは、少し高いレベルの統合テストほど優れた設計を促進するとは思いません。コードが適切に組み合わされて、開発チームと製品所有者の間で合意された望ましい動作の全体が生成されます。

これらのレベルでテストを作成できる場合、多くのクレイジーな依存関係を必要としない、素敵で論理的なAPIのようなコードを作成するようになります。クレイジーな依存関係または密結合コード。

間違いを犯さないでください- 単体テストは、良いデザインだけでなく、悪いデザインにもつながる可能性があります。開発者はすでに素敵な論理設計と単一の懸念を持っているコードを少し取って、それを引き離し、純粋にテストの目的でより多くのインターフェースを導入し、その結果コードを読みにくく変更しにくくしました開発者が多くの低レベルの単体テストを用意することで、より高レベルのテストを行う必要がないと判断した場合は、バグがさらに増える可能性もあります。特にお気に入りの例は、クリップボードの内外で情報を取得することに関連する非常に壊れた「テスト可能な」コードが多数あった場所で修正したバグです。すべてが細分化され、非常に小さなレベルの詳細に分離され、多くのインターフェイス、テストの多くのモック、およびその他の楽しいものがあります。1つだけ問題があります-OSのクリップボードメカニズムと実際に相互作用するコードはありませんでした。

単体テストは間違いなく設計を推進できますが、自動的に適切な設計に導くわけではありません。「このコードはテストされているため、テスト可能であり、それゆえ良い」ということを超えた、良いデザインとは何かについてのアイデアが必要です。

もちろん、「ユニットテスト」とは「UIを介さない自動テスト」を意味する人の場合、これらの警告の一部はあまり意味がないかもしれません-私が言ったように、レベルの統合テストは、多くの場合、設計の推進に関してより有用なテストです。


-2

ユニットテストは、新しいコードがすべての古いテストに合格した場合のリファクタリングに役立ちます。

急いでいてパフォーマンスに関心がないため、バブルソートを実装したとしますが、データが長くなっているため、クイックソートが必要になったとします。すべてのテストに合格すると、見栄えは良くなります。

もちろん、これを機能させるには、テストが包括的でなければなりません。私の例では、バブルソートの問題ではないため、テストで安定性がカバーされない場合があります。


1
これは事実ですが、コード設計の品質への直接的な影響よりも保守性のメリットが大きくなります。
アリP

@ AntP、OPはリファクタリングと単体テストについて尋ねました。
om

1
質問でリファクタリングについて言及しましたが、実際の質問は単体テストがどのようにコード設計を改善/検証できるかということでした。リファクタリングのプロセスを容易にするものではありません。
アントP

-3

ユニットテストは、プロジェクトの長期的なメンテナンスを促進するために最も価値があることがわかりました。数か月後にプロジェクトに戻って詳細をあまり覚えていない場合、テストを実行することで問題が解決しません。


6
それは確かにテストの重要な側面ですが、実際には質問に答えていません(テストが良い理由ではなく、デザインにどのように影響するかです)。
ハルク
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.