再設計時にテストを効率的に機能させるにはどうすればよいですか?


14

十分にテストされたコードベースには多くの利点がありますが、システムの特定の側面をテストすると、ある種の変更に耐えるコードベースが得られます。

例は、特定の出力(テキストまたはHTMLなど)のテストです。多くの場合、テストは、特定のテキストブロックを一部の入力パラメーターの出力として期待するため、またはブロック内の特定のセクションを検索するために(単純に?)書き込まれます。

新しい要件を満たすため、またはユーザビリティテストがインターフェイスの変更をもたらしたため、コードの動作を変更するには、テストも変更する必要があります。おそらく、変更されるコードの具体的な単体テストではないテストでさえもです。

  • これらのテストを見つけて書き換える作業をどのように管理しますか?「すべてを実行して、フレームワークでそれらを整理する」ことができない場合はどうでしょうか。

  • 他にどのようなテスト対象コードが習慣的に脆弱なテストになりますか?


これは、programmers.stackexchange.com / questions / 5898 / …とどう違うのですか?
AShelly

4
その質問は誤ってリファクタリングについて尋ねました。ユニットテストはリファクタリング下で不変でなければなりません。
アレックスファインマン

回答:


9

TDDの人々がこの答えを嫌うことは知っていますが、私にとってそれの大部分は、テストする場所を慎重に選択することです。

下位層の単体テストに夢中になりすぎると、単体テストを変更しない限り意味のある変更を加えることができません。インターフェイスが決して公開されておらず、アプリの外部で再利用されることを意図していない場合、これは、さもなければ迅速な変更であったかもしれないものに対する不必要なオーバーヘッドです。

逆に、変更しようとしているものが公開または再利用される場合、変更する必要があるこれらのテストのすべてが、他の場所で何かが壊れている証拠となります。

プロジェクトによっては、単体テストからではなく、受け入れ層からテストを設計することになります。ユニットテストが少なくなり、統合スタイルテストが増えます。

それは、その機能が受け入れ基準を満たすまで、単一の機能とコードを特定できないという意味ではありません。それは単に、場合によっては単体テストで受け入れ基準を測定しないことを意味します。


「アプリの外」ではなく、「モジュールの外」を書くつもりだったと思います。
SamB

SamB、それは依存します。インターフェースが1つのアプリを備えたいくつかの場所の内部であるが、公開されていない場合、インターフェースが不安定であると思われる場合は、より高いレベルでテストすることを検討します。
ビル

このアプローチはTDDと非常に互換性があることがわかりました。エンドユーザーに近いアプリケーションの上位層から開始するのが好きなので、下位層を使用するために上位層がどのように必要かを理解して下位層を設計できます。基本的にトップダウンで構築することにより、1つのレイヤーと別のレイヤー間のインターフェイスをより正確に設計できます。
グレッグブルクハート

4

TCP転送全体を書き換えて、SIPスタックの大規模なオーバーホールを完了しました。(これは、ほとんどのリファクタリングに比べて、かなり大規模な、ほぼリファクタリングでした。)

手短に言えば、TIdSipTransportのサブクラスであるTIdSipTcpTransportがあります。すべてのTIdSipTransportsは、共通のテストスイートを共有しています。TIdSipTcpTransportの内部には、接続/開始メッセージのペア、スレッド化されたTCPクライアント、スレッド化されたTCPサーバーなどを含むマップのような多くのクラスがありました。

私がやったことは次のとおりです。

  • 置き換えようとしていたクラスを削除しました。
  • それらのクラスのテストスイートを削除しました。
  • TIdSipTcpTransportにテストスイートの特定の(そしてすべてのTIdSipTransportsにテストスイートの一般的なのは、まだありました)。
  • TIdSipTransport / TIdSipTcpTransportテストを実行し、すべてが失敗したことを確認します。
  • 1つのTIdSipTransport / TIdSipTcpTransportテストを除くすべてをコメント化しました。
  • クラスを追加する必要がある場合は、コメントなしのテストが合格するのに十分な機能を構築するためのテストを記述します。
  • 泡立て、すすぎ、繰り返します。

このように、コメントアウトされたテスト(*)の形で私がまだ何をする必要があるかを知っており、書いた新しいテストのおかげで、新しいコードが期待どおりに機能していることを知っていました。

(*)本当に、コメントアウトする必要はありません。それらを実行しないでください。100のテストに失敗しても、あまり期待できません。また、私の特定のセットアップでは、コンパイルするテストの数が少ないということは、テスト-書き込み-リファクタリングのループが高速になることを意味します。


私も数ヶ月前にこれをやったことがあり、それは私にとって非常にうまくいった。ただし、ドメインモデルモジュールの基礎的な再設計(プロジェクト内の他のすべてのモジュールの再設計を引き起こした)で同僚とペアを組むときに、この方法を絶対に適用することはできませんでした。
マルコチャンブロン

3

テストが壊れやすい場合、間違ったものをテストしているため、通常はテストを見つけます。たとえば、HTML出力を考えます。実際のHTML出力を確認すると、テストは脆弱になります。しかし、あなたは実際の出力には興味がなく、それがすべき情報を伝えるかどうかに興味があります。残念ながら、それを行うにはユーザーの脳の内容についてアサーションを行う必要があるため、自動的に行うことはできません。

あなたはできる:

  • HTMLを煙テストとして生成し、実際に実行されることを確認します
  • テンプレートシステムを使用して、テンプレートプロセッサとテンプレートに送信されたデータをテストできます。実際にテンプレート自体を実際にテストする必要はありません。

SQLでも同じことが起こります。実際のSQLをアサートすると、クラスはあなたをトラブルに陥れようとします。あなたは本当に結果を主張したいです。したがって、ユニットテスト中にSQLITEメモリデータベースを使用して、SQLが想定どおりに実際に動作することを確認します。


また、構造HTMLを使用すると役立つ場合があります。
SamB

それが役立つだろう確かに@SamB、私はそれが問題を完全に解決しないと思う
ウィンストン・エバート

もちろんそうではありません。何もできません:-)
SamB

-1

まず、新しいAPIを作成します。これは、新しいAPIの動作にしたいことを行います。この新しいAPIの名前が古いAPIと同じである場合は、新しいAPI名に_NEWという名前を追加します。

int DoSomethingInterestingAPI();

になる:

int DoSomethingInterestingAPI_NEW(int takes_more_arguments); int DoSomethingInterestingAPI_OLD(); int DoSomethingInterestingAPI(){DoSomethingInterestingAPI_NEW(whatever_default_mimics_the_old_API); OK-この段階では、DoSomethingInterestingAPI()という名前を使用して、すべての回帰テストがパスします。

次に、コードを調べて、DoSomethingInterestingAPI()へのすべての呼び出しをDoSomethingInterestingAPI_NEW()の適切なバリアントに変更します。これには、新しいAPIを使用するために変更する必要がある回帰テストの部分の更新/書き換えが含まれます。

次に、DoSomethingInterestingAPI_OLD()を[[deprecated()]]としてマークします。必要な限り、非推奨のAPIを使用し続けます(それに依存する可能性のあるすべてのコードを安全に更新するまで)。

このアプローチでは、回帰テストの失敗は、単にその回帰テストのバグであるか、コードのバグを特定するだけです。この_NEWおよび_OLDバージョンのAPIを明示的に作成することにより、APIを改訂するこの段階的なプロセスにより、しばらくの間、新しいコードと古いコードの一部を共存させることができます。

実際のこのアプローチの良い(難しい)例がここにあります。関数BitSubstring()がありました-ここでは、3番目のパラメーターをサブストリング内のビットのCOUNTにするというアプローチを使用していました。C ++の他のAPIやパターンとの一貫性を保つために、関数の引数として開始/終了に切り替えたいと思いました。

https://github.com/SophistSolutions/Stroika/commit/003dd8707405c43e735ca71116c773b108c217c0

新しいAPIを使用して関数BitSubstring_NEWを作成し、それを使用するようにすべてのコードを更新しました(BitSubStringへの呼び出しはありません)。しかし、私はいくつかのリリース(月)で実装を残し、廃止予定としてマークしました。したがって、誰もがBitSubString_NEWに切り替えることができます(その時点で、引数をカウントから開始/終了スタイルに変更します)。

その後-その遷移が完了したら、BitSubString()を削除してBitSubString_NEW-> BitSubString()の名前を変更し、BitSubString_NEWという名前を廃止しました。


意味を持たない接尾辞、または名前に非推奨の接尾辞を追加しないでください。意味のある名前を付けるように常に努めてください。
バシレフ

あなたはポイントを完全に逃しました。まず、これらは「意味を持たない」接尾辞ではありません。APIが古いAPIから新しいAPIに移行しているという意味があります。実際、それは私が答えていた質問の全体のポイントであり、答えの全体のポイントです。名前は、OLD API、NEW API、および移行が完了すると最終的にAPIのターゲット名になることを明確に伝えます。AND-_OLD / _NEWサフィックスは一時的なものです-API変更の移行中のみ。
ルイスプリングル

3年後のAPIのNEW_NEW_3バージョンで頑張ってください。
バシレフ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.