web-dev-qa-db-ja.com

単体テストを有効にするためにコードを最初から設計する必要がありますか?

ユニットテストを許可するようにコードデザインを変更することがコードの匂いであるかどうか、またはコードの匂いなしでどの程度実行できるかについては、現時点でチームで議論が行われています。これは、他のほぼすべてのソフトウェア開発会社に存在するプラクティスを導入し始めたばかりであるために起こりました。

具体的には、非常に薄いWeb APIサービスを用意します。その主な責任は、Web要求/応答をマーシャリングし、ビジネスロジックを含む基になるAPIを呼び出すことです。

1つの例は、認証方法のタイプを返すファクトリを作成することです。インターフェースを継承する必要はありません。具体的なタイプ以外になるとは予想されていないからです。ただし、Web APIサービスを単体テストするには、このファクトリをモックする必要があります。

これは基本的に、DIを受け入れるように(そのコンストラクターまたはセッターを介して)Web APIコントローラークラスを設計することを意味します。つまり、コントローラーの一部を設計して、DIを許可し、他に必要のないインターフェイスを実装するか、またはNinjectのようなサードパーティのフレームワークでは、このようにコントローラーを設計する必要はありませんが、インターフェイスを作成する必要があります。

チームの何人かは、テストのためだけにコードを設計することに消極的であるようです。単体テストを希望する場合、多少の妥協が必要だと私には思われますが、彼らの懸念をどのように和らげるのかわかりません。

明確にするために、これはまったく新しいプロジェクトであるため、ユニットテストを有効にするためにコードを変更することではありません。単体テストができるように作成するコードを設計することです。

92
Lee

テストのためにコードを変更することをためらうことは、開発者がテストの役割を理解していないこと、そして暗示的に、組織における彼ら自身の役割を理解していないことを示しています。

ソフトウェアビジネスは、ビジネス価値を生み出すコードベースの提供を中心に展開しています。私たちは、長く苦い経験を​​通して、テストなしでは重要なサイズのそのようなコードベースを作成できないことを発見しました。したがって、テストスイートはビジネスの不可欠な部分です。

多くのコーダーはこの原則にリップサービスを支払いますが、無意識のうちにそれを受け入れません。これがなぜかは簡単に理解できます。私たち自身の精神的能力は無限ではなく、実際、現代のコードベースの非常に複雑な問題に直面したときに驚くほど制限されているという認識は歓迎されず、簡単に抑制または合理化されます。テストコードが顧客に提供されないという事実は、「必須」のビジネスコードと比較して、それが二級市民であり、必須ではないことを容易に信じさせます。そして、テストコードを追加するという考えビジネスコードには、多くの人にとって二重に不快に思えます。

この慣行を正当化することの問題は、ソフトウェアビジネスで価値がどのように生み出されるかという全体像が、企業階層の上位層によってのみ理解されることが多いという事実に関係していますが、これらの人々は詳細な技術的理解を持っていません理解するために必要なコーディングワークフローなぜテストを取り除くことはできません。したがって、彼らは、テストが一般的に良いアイデアであるかもしれないことを彼らに保証する開業医によってあまりに頻繁に平静にされるbut「私たちはそのような松葉杖を必要としないエリートプログラマー」、または「私たちはしない」ビジネスの成功は数字のゲームであり、技術的な負債を回避し、品質を保証することなどがその価値を長期的にのみ示すという事実は、彼らがその信念を非常に誠実であることが多いことを意味します。

簡単に言えば、コードをテスト可能にすることは、他の分野と同じように、開発プロセスの不可欠な部分です(多くのマイクロチップは、かなりの割合の要素で設計されていますonlyテスト目的で)、それは非常に簡単ですそのための非常に良い理由を見落とすために。その罠に陥らないでください。

203
Kilian Foth

思っているほど簡単ではありません。分解してみましょう。

  • 単体テストの作成は間違いなく良いことです

だが!

  • コードを変更すると、バグが発生する可能性があります。したがって、適切なビジネス上の理由なしにコードを変更することは良い考えではありません。

  • 「非常に薄い」webapiは、単体テストの最高のケースのようには見えません。

  • コードとテストを同時に変更することは悪いことです。

次のアプローチをお勧めします。

  1. 統合テストを記述します。これにはコードの変更は必要ありません。これにより、基本的なテストケースが提供され、コードをさらに変更してもバグが発生しないことを確認できます。

  2. 新しいコードがテスト可能であり、ユニットおよび統合テストがあることを確認してください。

  3. ビルドとデプロイ後にCIチェーンがテストを実行することを確認してください。

これらの設定が完了したら、それから初めて、テスト可能性のためにレガシープロジェクトをリファクタリングすることを考え始めます。

うまくいけば、誰もがプロセスから教訓を学び、テストが最も必要とされる場所、それをどのように構成するか、そしてビジネスにもたらす価値について、良い考えを持っているでしょう。

[〜#〜] edit [〜#〜]:この回答を書いてから、OPは質問を明確にして、既存のコードの変更ではなく新しいコードについて話していることを示しました。 「単体テストはいいのか?」議論は数年前に解決されました。

ユニットテストで必要なコードの変更を想像するのは難しいですが、どのような場合でも望ましい一般的な方法ではありません。おそらく、実際の反対意見を検討するのが賢明でしょう。反対されているのは、おそらくそれが単体テストのスタイルだからです。

75
Ewan

本質的にテスト可能なコードを設計することは、コードの匂いではありません。それどころか、それは良いデザインのしるしです。これに基づいた有名で広く使用されているいくつかのデザインパターン(Model-View-Presenterなど)があり、大きな利点として簡単な(簡単な)テストを提供します。

したがって、より簡単にテストするために具象クラスのインターフェースを作成する必要がある場合、それは良いことです。すでに具象クラスがある場合、ほとんどのIDEはそこからインターフェースを抽出できるため、必要な労力を最小限に抑えることができます。この2つを同期させるのは少し手間がかかりますが、インターフェイスはそれほど大きく変更されるべきではなく、テストによるメリットはその余分な労力を上回る可能性があります。

一方、@ MatthieuMとして。コメントで言及されていますが、テストのためだけに本番環境で使用してはならない特定のエントリポイントをコードに追加する場合は、問題になる可能性があります。

18
mmathis

ユニットテストを作成するには、テストするコードに少なくとも特定のプロパティが必要であることを理解するのは非常に簡単です。たとえば、コードがindividual unitsで構成されていない場合、個別にテストできますが、「ユニットテスト」という単語は意味を成しません。コードにこれらのプロパティがない場合は、最初に変更する必要があります。これはかなり明白です。

理論的には、最初にテスト可能なコードユニットを記述して、SOLIDの原則をすべて適用し、その後、元のコードをさらに変更することなく、そのテストを記述してみることができます。残念ながら、実際に単体テストが可能なコードを書くことは必ずしも簡単なことではないため、テストを作成しようとしたときにのみ検出される変更が必要になる可能性が高いです。これは、アイデアを使用して作成された場合でもコードに当てはまります。ユニットテストのことを念頭に置いてください。「ユニットテストの可能性」が最初の議題に含まれていなかった場合に書かれたコードには、間違いなく真実です。

最初にユニットテストを記述して問題を解決しようとするよく知られたアプローチがあります。これはテスト駆動開発(TDD)と呼ばれ、コードを最初からよりユニットテスト可能にするのに役立ちます。

もちろん、後でコードを変更してテスト可能にすることに抵抗があるのは、コードを最初に手動でテストしたり、製品で正常に機能したりする状況でよく発生するため、コードを変更すると実際に新しいバグが発生する可能性があるためです。これを軽減するための最良のアプローチは、最初に回帰テストスイート(コードベースに最小限の変更を加えるだけで実装できることが多い)と、コードレビューなどの付随する他の対策、または新しい手動テストセッションを作成することです。それはあなたがいくつかの内部を再設計することが重要なものを壊さないことを確実にするのに十分な自信を与えるべきです。

13
Doc Brown

私はあなたが作る(根拠のない)主張に問題を抱えています:

web APIサービスを単体テストするには、このファクトリをモックする必要があります

それは必ずしも本当ではありません。テストを書く方法はたくさんあり、モックを使わない単体テストを書く方法はareあります。さらに重要なことに、機能テストや統合テストなどの他のkindsテストがあります。多くの場合、OOPプログラミング言語interfaceではない「インターフェース」で「テストシーム」を見つけることができます。

より自然かもしれない、別のテストシームを見つけるのに役立ついくつかの質問:

  • different APIを介してシンWeb APIを記述したいですか?
  • Web APIと基になるAPIの間のコードの重複を減らすことはできますか?一方を他方に関して生成できますか?
  • Web API全体と基になるAPIを1つの「ブラックボックス」ユニットとして扱い、全体の動作について意味のあるアサーションを作成できますか?
  • 将来、Web APIを新しい実装に置き換える必要がある場合、どうすればよいでしょうか。
  • Web APIが将来新しい実装に置き換えられた場合、Web APIのクライアントは気付くことができますか?もしそうなら、どうですか?

あなたが作るもう一つの根拠のない主張はDIについてです:

(コンストラクターまたはセッターを介して)DIを受け入れるようにWeb APIコントローラークラスを設計します。つまり、DIを許可するためだけにコントローラーの一部を設計し、それ以外には必要のないインターフェースを実装するか、サードパーティを使用します。 Ninjectのようなフレームワークでは、このようにコントローラーを設計する必要はありませんが、インターフェイスを作成する必要があります。

依存性注入は、必ずしも新しいinterfaceを作成することを意味しません。たとえば、認証トークンの場合:プログラムでreal認証トークンを作成できますか?次に、テストはそのようなトークンを作成し、それらを注入できます。トークンを検証するプロセスは、何らかの暗号シークレットに依存していますか?シークレットをハードコーディングしていないことを願っています-なんとかしてストレージからそれを読み取ることができると思います。その場合、テストケースで別の(既知の)シークレットを使用できます。

これは、新しいinterfaceを作成してはならないということではありません。ただし、テストを作成する1つの方法、または動作を偽造する1つの方法しかないことに固執しないでください。ボックスの外側で考える場合は、通常、コードのゆがみを最小限に抑えながら、必要な効果が得られるソリューションを見つけることができます。

11
Daniel Pryden

これは新しいプロジェクトなので、幸運です。 Test Driven Designは優れたコードを書くのに非常にうまく機能することがわかりました(そのため、そもそもそれを行います)。

前もって現実的な入力データで特定のコードを呼び出し、確認できる現実的な出力データを取得する方法を理解することで、プロセスの非常に早い段階でAPI設計を行い、適応させるために書き直さなければならない既存のコードに邪魔されないので、有用な設計を得る良いチャンスがあります。また、同僚が理解しやすくなるため、プロセスの早い段階で良い議論を再び行うことができます。

上記の文で「有用」とは、結果として得られるメソッドが簡単に呼び出せるだけでなく、統合テストで簡単に作成でき、モックアップを作成できるクリーンなインターフェースを取得する傾向があることを意味します。

考慮して下さい。特に査読付き。私の経験では、時間と労力の投資はすぐに返されます。

コードを変更する必要がある場合、それはコードのにおいです。

個人的な経験から、私のコードがテストを書くことが難しい場合、それは悪いコードです。動作しないか、設計どおりに機能しないため、それは悪いコードではありません。なぜそれが機能するのかをすぐに理解できないため、それは悪いコードです。バグに遭遇した場合、それを修正するのは長い間骨の折れる仕事になることを私は知っています。コードを再利用することも困難/不可能です。

優れた(クリーン)コードは、タスクを小さなセクションに分割し、一目で(または少なくとも見栄えが)簡単に理解できます。これらの小さなセクションのテストは簡単です。また、サブセクションにかなり自信がある場合は、コードベースのチャンクのみを同じように簡単にテストするテストを作成することもできます(すでにテスト済みなので、再利用も役立ちます)。

コードをテストしやすく、リファクタリングしやすく、最初から再利用しやすくしておくと、変更が必要なときにいつでも自分を殺してしまうことはありません。

私は使い捨てのプロトタイプであるはずのプロジェクトをよりクリーンなコードに完全に再構築しながら、これを入力しています。部分的に動作するものを壊すのを恐れて何かに触れるのを恐れて画面を何時間もじっと見つめるよりも、最初から正しく、できるだけ早く不良コードをリファクタリングする方がはるかに優れています。

8
David

できないユニットテストされるコードを書くことはコードの匂いだと私は主張します。一般に、コードを単体テストできない場合、コードはモジュール化されていないため、理解、維持、または拡張が困難になります。おそらく、コードが統合テストの点で本当に意味のあるグルーコードである場合、統合テストを単体テストに置き換えることができますが、それでも統合が失敗した場合、問題を分離する必要があり、単体テストはやれ。

あなたは言う

認証方式タイプを返すファクトリを作成する予定です。インターフェースを継承する必要はありません。具体的なタイプ以外になるとは予想されていないからです。ただし、Web APIサービスを単体テストするには、このファクトリをモックする必要があります。

私は本当にこれに従いません。何かを作成するファクトリーを用意する理由は、ファクトリーを変更したり、ファクトリーが簡単に作成するものを変更したりできるため、コードの他の部分を変更する必要がないためです。認証方法が決して変更されないのであれば、ファクトリーは役に立たないコード膨張です。ただし、テスト環境で本番環境とは異なる認証方法を使用する場合は、テスト環境で本番環境とは異なる認証方法を返すファクトリを用意することは、優れたソリューションです。

これにはDIやモックは必要ありません。ファクトリが必要とするのは、さまざまな認証タイプをサポートし、構成ファイルや環境変数などから構成できるようにすることだけです。

4
Old Pro

私が考えることができるすべてのエンジニアリング分野で、まともなまたはより高いレベルの品質を達成する方法は1つだけです。

設計における検査/テストを説明します。

これは、構築、チップ設計、ソフトウェア開発、および製造に当てはまります。だからといって、テストがすべての設計を構築するための柱であるということではありません。しかし、すべての設計決定において、設計者はテストコストへの影響を明確にし、トレードオフについて意識的な決定を行う必要があります。

場合によっては、単体テストよりも手動または自動(Seleniumなど)のテストの方が便利ですが、単体テストでも許容範囲のテストを提供できます。まれに、ほぼ完全にテストされていないものを投げ出すことも許容できる場合があります。しかし、これらはケースバイケースの意思決定を意識する必要があります。 「コードのにおい」のテストを説明するデザインを呼び出すことは、深刻な経験不足を示します。

2
Peter

これは基本的に、DIを受け入れるように(そのコンストラクターまたはセッターを介して)Web APIコントローラークラスを設計することを意味します。つまり、コントローラーの一部を設計して、DIを許可し、他に必要のないインターフェイスを実装するか、またはNinjectのようなサードパーティのフレームワークでは、このようにコントローラーを設計する必要はありませんが、インターフェイスを作成する必要があります。

テスト可能なものの違いを見てみましょう:

public class MyController : Controller
{
    private readonly IMyDependency _thing;

    public MyController(IMyDependency thing)
    {
        _thing = thing;
    }
}

およびテスト不可能なコントローラー:

public class MyController : Controller
{
}

前者のオプションには、文字どおり5行の追加コードがあり、そのうち2行はVisual Studioで自動生成できます。依存関係注入フレームワークをセットアップして、実行時にIMyDependencyを具象型に置き換える-これはまともなDIフレームワークの場合、別の1行のコードです-すべて正常に機能しますが、今はモックしてテストすることができますコントローラーを心ゆくまでご利用ください。

テスト可能にするための6行の追加コード...そしてあなたの同僚は、それは「あまりにも多くの仕事」だと主張していますか?その議論は私と一緒に飛ぶわけではありませんし、あなたと一緒に飛ぶべきではありません。

そして、テスト用のインターフェースを作成して実装する必要はありませんMoq たとえば、ユニットテストのための具象型の振る舞い。もちろん、テストしているクラスにそれらの型を注入できない場合、これはあまり役に立ちません。

依存性注入は、いったん理解すると、「これなしでどのように機能したのか」と疑問に思うものの1つです。それはシンプルで、効果的で、意味をなすだけです。プロジェクトをテスト可能にするために、同僚が新しいことを理解していないことを許さないでください。

1
Ian Kemp

単体テスト(および他の種類の自動テスト)は、コードのにおいを減らす傾向があることを発見しました。コードのにおいを導入する1つの例は考えられません。ユニットテストは通常​​、より良いコードを書くことを強制します。テスト中のメソッドを簡単に使用できない場合、コード内でメソッドを簡単にする必要があるのはなぜですか?

よく書かれた単体テストは、コードの使用方法を示しています。それらは実行可能なドキュメントの形式です。単純に理解できなかった、ひどく書かれた、非常に長い単体テストを見たことがあります。それらを書かないでください!クラスをセットアップするために長いテストを作成する必要がある場合は、クラスをリファクタリングする必要があります。

単体テストでは、コードの匂いがどこにあるかを強調表示します。 Michael C. Feathersのレガシーコードを効果的に使用するを読むことをお勧めします。プロジェクトが新しい場合でも、ユニットテストがまだ(または多く)ない場合は、コードを適切にテストするために自明ではない手法が必要になる場合があります。

1
CJ Dennis

手短に:

テスト可能なコードは(通常)メンテナンス可能なコードです。つまり、テストがハードであるコードは、通常ハードがメンテナンスが難しいコードです。テスト不可能なコードを設計することは、修復不可能なマシンを設計することと同じです-最終的にそれを修復するために /が割り当てられる貧しいshmuckに同情してください(それはあなたかもしれません)。

1つの例は、認証方法のタイプを返すファクトリを作成することです。インターフェースを継承する必要はありません。具体的なタイプ以外になるとは予想されていないからです。

3年後に5種類の認証方法タイプが必要になることをご存じでしょう。要件は変化し、設計の過剰設計を回避する必要がありますが、testable設計は、(なし)なしで変更できる十分な継ぎ目があることを意味します多くの)痛み-そして、モジュールテストはあなたの変更が何も壊さないことを確認するための自動化された手段をあなたに提供するでしょう。

1
CharonX

依存性注入に関する設計はコードの匂いではありません-それはベストプラクティスです。 DIの使用は、テスト容易性のためだけのものではありません。 DIを中心にコンポーネントを構築すると、モジュール性と再利用性が向上し、主要コンポーネント(データベースインターフェイスレイヤーなど)をより簡単に交換できるようになります。ある程度の複雑さを追加しますが、正しく実行すると、レイヤーの分離と機能の分離が向上し、複雑さの管理とナビゲートが容易になります。これにより、各コンポーネントの動作を適切に検証してバグを減らしやすくなり、バグの追跡も容易になります。

1
Zenilogix

単体テストを作成するとき、コード内で何が問題になるのかを考え始めます。コード設計を改善し、 単一責任の原則 (SRP)を適用するのに役立ちます。また、数か月後に同じコードを変更するために戻ってきたときに、既存の機能が壊れていないことを確認するのに役立ちます。

できる限り純粋な関数を使用する傾向があります(サーバーレスアプリ)。単体テストは、状態を分離して純粋な関数を書くのに役立ちます。

具体的には、非常に薄いWeb APIサービスを用意します。その主な責任は、Web要求/応答をマーシャリングし、ビジネスロジックを含む基になるAPIを呼び出すことです。

最初に、基盤となるAPIの単体テストを記述します。十分な開発時間がある場合は、シンWeb APIサービスのテストも記述する必要があります。

TL; DR、単体テストは、コードの品質を向上させ、コードの将来の変更をリスクなしで行うのに役立ちます。また、コードが読みやすくなります。あなたの主張をするためにコメントの代わりにテストを使用してください。

0
Ashutosh

肝心な点、そして消極的で多くのあなたの議論であるべきことは、衝突がないということです。大きな間違いは、誰かが「テスト用に設計する」という考えを、テストを嫌う人々に作り出したということです。 「時間をかけてこれを正しくやろう」のように、彼らは口を閉めるか、言葉を変えるだけでいいのです。

何かをテスト可能にするには「インターフェースを実装しなければならない」という考えは間違っています。インターフェースはすでに実装されていますが、クラス宣言ではまだ宣言されていません。それは、既存のパブリックメソッドを認識し、そのシグネチャをインターフェイスにコピーし、クラスの宣言でそのインターフェイスを宣言することです。プログラミングなし、既存のロジックへの変更なし。

どうやら一部の人々はこれについて異なる考えを持っています。最初にこれを修正することをお勧めします。

0
Martin Maat