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


20

バックグラウンド

テスト駆動開発は、私がすでに学校を卒業した後、業界で普及しました。私はそれを学ぼうとしていますが、いくつかの大きな事柄がまだ私を逃れています。TDDの支持者は、次のような多くのことを言います(以降、「単一アサーション原則」またはSAPと呼びます)。

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

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

彼らはまた、このようなことを言います(以下「プライベートメソッドの原則」またはPMPと呼ばれます):

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

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

ソース:プライベートメソッドを単体テストする方法

状況

ステートフルデータ処理システムをテストしようとしています。システムは、データを受け取る前の状態を考えると、まったく同じデータに対して異なることを行うことができます。システムに状態を​​構築し、特定のメソッドがテストすることを意図した動作をテストする簡単なテストを検討してください。

  • SAPは、「状態ビルドアッププロシージャ」をテストするべきではないことを提案します。状態はビルドアップコードから予想されるものであると想定し、テストしようとしている1つの状態変化をテストします。

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

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


2
これに対するエレガントな解決策が見つかるとは思わない。一般的なアプローチは、システムを最初からステートフルにすることではありません。これは、既に構築されているものをテストするときに役立ちません。ステートレスになるようにリファクタリングすることは、おそらくコストの価値もありません。
ドーバル


@Doval:電話(SIP UserAgent)のようなものを非ステートフルにする方法を説明してください。このユニットの予想される動作は、状態遷移図を使用してRFCで指定されています。
バートヴァンインゲンシェナウ

テストをコピー/貼り付け/編集していますか、またはセットアップ/分解/機能を共有するユーティリティメソッドを作成していますか?一部のテストケースは確かに長く肥大化する可能性がありますが、これはそれほど一般的ではありません。ステートフルシステムでは、最終状態がパラメーターであり、このルーチンがテストする状態に到達する一般的なセットアップルーチンが必要です。さらに、各テストの終了時に既知の開始状態に戻すティアダウンメソッドがあり(必要な場合)、次のテストの開始時にセットアップメソッドが適切に機能します。
ダンク

接線についてですが、状態図はコミュニケーションツールであり、RFCにある場合でも実装の法令ではないことも付け加えます。説明した機能を満たす限り、標準を満たします。(RFCで定義されている)本当に複雑な状態遷移の実装を、本当に単純な一般的な処理機能に変換したことが何度かありました。「隠された」共通要素の名前を変更すると、5つの状態に関するいくつかのフラグ以外はまったく同じことを実現した後、数千行のコードを削除したことを覚えています。
ダンク

回答:


15

視点:

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

細部に迷い、私たちが達成しようとしているものを見失ってしまうのは簡単だと思うからです。

原則-SAP:

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

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

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

原則-PMP:

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

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


応用TDD(あなたのために

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

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

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

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

私が言及したいそのアプローチの背後にある含意があります。 ネタバレ、これは私が純粋主義者を激怒させる場所です

まず、ある状況ではテストとして、別の状況ではセットアップとして何かを使用することを受け入れる必要があります。一方で、これはSAPの直接的な違反のようです。しかし、論理的にChangeFooToState12つの目的を持っていると考えると、SAPが教えている精神を満たしていることになります。Foo()状態の変更を確認する必要がある場合ChangeFooToState1は、テストとして使用します。また、「Foo()」の「」の出力を検証する必要がある場合は、セットアップとしてState1使用ChangeFooToState1しています。

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

それを一緒に入れて:

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

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

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


11

通常、セットアップの詳細を機能に抽象化するので、繰り返す必要はありません。そうすれば、機能が変更された場合に、テスト内の1か所で変更するだけで済みます。

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

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

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

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


記録のために、SIPの例は私のものであり、OPからのものではありません。また、一部のステートマシンでは、特定のテストに適切な状態にするために、数回以上のメソッド呼び出しが必要になる場合があります。
バートヴァンインゲンシェナウ

「状態を完全に回避することはできませんが、最小化して分離することはできます。」私は同意できませんでした。状態はソフトウェアに必要な悪です。
ブランドン

0

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

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.