web-dev-qa-db-ja.com

ステートフルシステムの単体テストの設計

バックグラウンド

テスト駆動開発 は、私がすでに学校を卒業した後、業界で普及しました。私はそれを学ぼうとしていますが、いくつかの主要なことはまだ私から逃れています。 TDD支持者は、次のような多くのことを述べています(以下、「単一アサーション原則」または[〜#〜] sap [〜#〜]と呼びます))。

しばらくの間、TDDテストを可能な限り単純に、表現力豊かに、そしてエレガントにすることができるかについて考えてきました。この記事では、テストを可能な限り単純化し、分解するのがどのようなものかについて少し探ります。各テストで1つのアサーションを目指します。

ソース: http://www.artima.com/weblogs/viewpost.jsp?thread=35578

彼らはまた、次のようなことも言っています(以下、「プライベートメソッドの原則」または[〜#〜] pmp [〜#〜]と呼びます)。

通常、プライベートメソッドを直接単体テストすることはありません。それらはプライベートなので、実装の詳細を考慮してください。それらの1つを呼び出して、それが特定の方法で機能することを期待する人は誰もいません。

代わりに、パブリックインターフェイスをテストする必要があります。プライベートメソッドを呼び出すメソッドが期待どおりに機能している場合、プライベートメソッドが正しく機能していることを拡張で仮定します。

出典: プライベートメソッドを単体テストするにはどうすればよいですか?

状況

ステートフルなデータ処理システムをテストしようとしています。システムは、データを受信する前の状態を前提として、まったく同じデータに対して異なる処理を実行できます。システムの状態を構築する単純なテストを考え、次に、指定されたメソッドがテストすることを意図されている動作をテストします。

  • SAPは、「状態構築手順」をテストするべきではないことを示唆しています。状態は、ビルドコードから期待されるものであると想定し、テストしようとしている1つの状態変更をテストします。

  • PMPは、この「状態構築」ステップをスキップして、その機能を個別に制御するメソッドをテストすることはできないことを示唆しています。

私の実際のコードの結果は、肥大化し、複雑で、長く、そして書くのが難しいテストでした。そして、状態遷移が変化した場合、テストを変更する必要があります...小さく効率的なテストでは問題ありませんが、非常に時間がかかり、これらの長い肥大したテストと混同します。これは通常どのように行われますか?

20
durron597

視点:

それでは、一歩下がって、TDDが私たちを助けようとしているものを尋ねましょう。 TDDは、コードが正しいかどうかを判断するのに役立ちます。正確には、「コードはビジネス要件を満たしていますか」という意味です。セールスポイントは、将来的に変更が必要になることがわかっていることです。これらの変更を行った後も、コードが正しいことを確認したいのです。

細部に行き詰まり、私たちが達成しようとしていることを見失うのは簡単だと思うので、私はその視点を持ち出します。

原則-SAP:

私はTDDの専門家ではありませんが、単一アサーション原則(SAP)が教えようとしていることの一部が欠けていると思います。 SAPは「一度に1つずつテストする」と言い換えることができます。しかし、TOTATはSAPほど簡単には口から出ません。

一度に1つずつテストするということは、1つのケースに集中することを意味します。 1つのパス。 1つの境界条件。 1つのエラーケース。テストごとに1つwhatever。そして、その背後にある原動力となる考えは、テストケースが失敗したときに何が壊れたかを知る必要があるため、問題をより迅速に解決できるということです。テスト内で複数の条件(つまり、複数のもの)をテストし、テストが失敗した場合、手に多くの作業が必要になります。最初に、複数のケースのどれが失敗したかを特定し、次にthatケースが失敗した理由を理解する必要があります。

一度に1つずつテストすると、検索範囲が大幅に縮小され、欠陥がより迅速に特定されます。 「一度に1つのことをテストする」とは、必ずしも一度に複数のプロセス出力を見ることができないわけではないことに注意してください。たとえば、「既知の適切なパス」をテストする場合、fooの特定の結果の値とbarの別の値が表示され、_foo != bar_テストの一部として。重要なのは、テストされるケースに基づいて出力チェックを論理的にグループ化することです。

原則-PMP:

同様に、Private Method Principle(PMP)が私たちに何を教える必要があるかについて、少し不足していると思います。 PMPは、システムをブラックボックスのように扱うことを推奨しています。特定の入力に対して、特定の出力を取得する必要があります。ブラックボックスがどのように出力を生成するかは問題ではありません。あなたはあなたの出力があなたの入力と整列することだけを気にします。

PMPは、コードのAPIの側面を見るのに非常に優れた視点です。また、テストする必要のある対象を絞り込むのにも役立ちます。インターフェースポイントを特定し、それらが契約の条件を満たしていることを確認します。インターフェースの背後にある(別名プライベート)メソッドがどのように機能するかを気にする必要はありません。あなたは彼らが彼らがすることになっていたことをしたことを確認する必要があるだけです。


適用されたTDD(あなたのために

そのため、あなたの状況は、通常のアプリケーションを超える少ししわを示します。アプリのメソッドはステートフルであるため、その出力は入力だけでなく、以前に行われた内容にも依存します。状態が恐ろしくて何とか何とかしてここに_<insert some lecture>_すべきだと私は確信していますが、それは本当にあなたの問題を解決するのに役立ちません。

さまざまな潜在的な状態と、遷移をトリガーするために何を実行する必要があるかを示す、ある種の状態図表があると仮定します。そうしないと、このシステムのビジネス要件を表現するのに役立つため、必要になります。

The Tests:最初に、状態の変化を実行する一連のテストで終了します。理想的には、発生する可能性のあるすべての状態変化を実行するテストがあることになりますが、その範囲を十分に超える必要がないシナリオがいくつかあります。

次に、データ処理を検証するためのテストを作成する必要があります。これらの状態テストの一部は、データ処理テストを作成するときに再利用されます。たとえば、Initと_State1_の状態に基づいて異なる出力を持つメソッドFoo()があるとします。 "Foo()が_ChangeFooToState1_にある場合の出力をテストするために、セットアップステップとして_State1_テストを使用する必要があります。

私が言及したいのは、そのアプローチの背後にいくつかの含意があります。 スポイラー、私は純粋主義者を激怒させます

まず、ある状況ではテストとして何かを使用し、別の状況ではセットアップとして使用することを受け入れる必要があります。一方では、これはSAPの直接の違反のようです。しかし、_ChangeFooToState1_を2つの目的として論理的に構成すると、SAPが教えている精神にまだ応えることができます。 Foo()が状態を変更することを確認する必要がある場合は、テストとして_ChangeFooToState1_を使用します。そして、「_State1_にあるときにFoo()の出力を検証する必要がある場合、セットアップとして_ChangeFooToState1_を使用しています。

2番目の項目は、実用的な観点からは、システムに対して完全にランダム化された単体テストを必要としないことです。出力検証テストを実行する前に、すべての状態変更テストを実行する必要があります。 SAPは、その注文の背後にある基本原則です。何が明白かを述べる-テストとして失敗した場合、セットアップとして何かを使用することはできません。

それを一緒に入れて:

状態図を使用して、遷移をカバーするテストを生成します。ここでも、ダイアグラムを使用して、状態によって駆動されるすべての入力/出力データ処理ケースをカバーするテストを生成します。

このアプローチに従うと、_bloated, complicated, long, and difficult to write_テストの管理が少し簡単になります。一般に、それらは最終的に小さくなり、より簡潔になります(つまり、複雑さが軽減されます)。テストはより分離されているか、モジュール化されていることにも注意してください。

さて、良いテストを書くのに多少の労力が必要になるため、プロセスが完全に簡単になるとは言いません。また、かなりの数のケースで2番目のパラメーター(状態)をマッピングしているため、それらのいくつかは依然として困難です。余談ですが、ステートレスシステムの方がテストを構築しやすい理由はもう少しはっきりしているはずです。しかし、このアプローチをアプリケーションに適合させると、アプリケーションが正しく機能していることを証明できることがわかります。

15
user53019

通常は、セットアップの詳細を関数に抽象化して、自分で繰り返す必要がないようにします。これにより、機能が変更された場合でも、テストの1か所で変更するだけで済みます。

ただし、通常はセットアップ機能でさえ、肥大化、複雑化、または長期化するようなことは避けたいと思います。これは、インターフェイスのリファクタリングが必要であることを示しています。テストを使用することが難しい場合、実際のコードも使用することが難しいためです。

これは、多くの場合、1つのクラスに多くを入れすぎていることを示しています。ステートフルな要件がある場合は、状態を管理するクラスだけが必要です。それをサポートするクラスは、ステートレスである必要があります。 SIPの例の場合、パケットの解析は完全にステートレスである必要があります。パケットを解析してからsipStateController.receiveInvite()のようなものを呼び出して、状態遷移を管理するクラスを持つことができます。電話を鳴らすなどのことを行うために他のステートレスクラスを呼び出します。

これにより、ステートマシンクラスのユニットテストのセットアップは、数回のメソッド呼び出しで簡単に行えます。ステートマシンの単体テストのセットアップでパケットの作成が必要な場合は、そのクラスに入れすぎています。同様に、パケットパーサークラスは、ステートマシンクラスのモックを使用して、セットアップコードを作成するのが比較的簡単である必要があります。

つまり、状態を完全に回避することはできませんが、状態を最小化して分離することはできます。

11
Karl Bielefeldt

TDDの核となる考え方は、最初にテストを記述することで、少なくともテストが容易なシステムになるということです。うまくいけば、機能し、保守可能であり、十分に文書化されているなどですが、そうでない場合でも、少なくともテストは簡単です。

そのため、TDDを実行して、テストがhardのシステムになってしまう場合は、何か問題が発生しています。おそらく、プライベートなものは公開する必要があります。テスト用に必要だからです。おそらく、適切な抽象化レベルで作業していません。リストのように単純なものは、あるレベルではステートフルですが、別のレベルでは値です。または、コンテキストに当てはまらないアドバイスに重みを付けすぎているか、問題が難しいだけかもしれません。または、もちろん、おそらくあなたのデザインは単に悪いです。

原因が何であれ、簡単なテストコードでシステムをテストしやすくするために戻ってシステムを再度作成することはおそらくないでしょう。そのため、おそらく最良の計画は、次のような、もう少し高度なテスト手法を使用することです。

0
soru