脆弱な単体テストを回避する方法は?


24

3,000件近くのテストを作成しました。データはハードコーディングされており、コードの再利用はほとんどありません。この方法論は、私たちを尻に刺し始めました。システムが変更されると、壊れたテストの修正により多くの時間を費やすことになります。ユニット、統合、機能テストがあります。

私が探しているのは、管理しやすく保守可能なテストを書くための決定的な方法です。

フレームワーク


これは、はるかに優れた、IMO ... Programmers.StackExchangeに適している
IAbstract

回答:


21

それらはそうではないので、「壊れた単体テスト」と考えないでください。

これらは仕様であり、プログラムでサポートされなくなりました。

「テストの修正」ではなく、「新しい要件の定義」と考えてください。

テストでは、最初にアプリケーションを指定する必要があり、その逆ではありません。

動作することがわかっているまで、動作する実装があるとは言えません。テストするまで機能するとは言えません。

あなたを導くかもしれないいくつかの他のメモ:

  1. テストテスト対象のクラスは、短くてシンプルでなければなりません。各テストでは、まとまりのある機能のみをチェックする必要があります。つまり、他のテストがすでにチェックしていることは気にしません。
  2. テストとオブジェクトは疎結合である必要があります。オブジェクトを変更すると、依存関係グラフが下方向にのみ変更され、そのオブジェクトを使用する他のオブジェクトは影響を受けません。
  3. 間違ったものを作成してテストしている可能性があります。あなたのオブジェクトは、簡単なインターフェース、または簡単な実装のために構築されていますか?後者の場合、古い実装のインターフェースを使用する多くのコードを変更することになります。
  4. 最良の場合、単一責任の原則を厳密に順守します。最悪の場合、インターフェイス分離の原則を順守してください。SOLID Principlesを参照してください。

5
+1Don't think of it as "fixing the tests", but as "defining new requirements".
StuperUser

2
+1 テストでは、アプリケーションを指定する必要があります。逆ではなく、
ツリーコーダー

11

あなたが説明することは実際にはそれほど悪いことではないかもしれませんが、あなたのテストが発見するより深い問題へのポインタ

システムが変更されると、壊れたテストの修正により多くの時間を費やすことになります。ユニット、統合、機能テストがあります。

コードを変更でき、テストが中断しない場合、それは私にとって疑わしいことです。正当な変更とバグの違いは、それが要求されているという事実だけであり、要求されたものはテストによって定義されます(TDDを想定)。

データはハードコーディングされています。

テストでハードコードされたデータは良いものです。テストは、証明としてではなく、改ざんとして機能します。計算が多すぎる場合、テストはトートロジーになります。例えば:

assert sum([1,2,3]) == 6
assert sum([1,2,3]) == 1 + 2 + 3
assert sum([1,2,3]) == reduce(operator.add, [1,2,3])

抽象化が高ければ高いほど、アルゴリズムに近づき、それにより、実際の実装とそれ自体を比較することに近づきます。

コードの再利用はほとんどありません

assertThatテストを簡単に保つため、テストでのコードの最適な再利用はjUnitsのように「チェック」です。それに加えて、コードを共有するためにテストをリファクタリングできる場合、テストされる実際のコードも多すぎる可能性があるため、リファクタリングされたベースをテストするものにテストを減らします。


ダウンボッターがどこで意見が合わないか知りたい。
ケプラ

keppla-私は支持者ではありませんが、一般に、モデルのどこにいるかにもよりますが、ユニットレベルでのデータのテストよりもオブジェクトの相互作用のテストを優先します。データのテストは、統合レベルでより適切に機能します。
リッチメルトン

@keppla合計アイテムに特定の制限付きアイテムが含まれている場合に、注文を別のチャネルにルーティングするクラスがあります。偽の注文を作成し、4つのアイテムを入れます。そのうちの2つは制限されたアイテムです。制限されたアイテムが追加される限り、このテストは一意です。ただし、偽の注文を作成し、2つの通常のアイテムを追加する手順は、制限のないアイテムのワークフローをテストする別のテストが使用するのと同じ設定です。この場合、注文に顧客データのセットアップとアドレスのセットアップなどが必要な場合はアイテムとともに、セットアップヘルパーを再利用するこの良いケースではありません。なぜ再利用のみを主張するのですか?
Asifシラーズ

6

私もこの問題を抱えています。私の改善されたアプローチは次のとおりです。

  1. 単体テストが何かをテストする唯一の良い方法でない限り、単体テストを書かないでください。

    単体テストの診断と修正にかかる時間のコストが最も低いことを認める用意ができています。これは彼らを貴重なツールにします。問題は、明らかなようにマイレージが変わる可能性があることですが、単体テストはコード量を維持するコストに見合わないことが多いということです。一番下に例を書いて、見てみましょう。

  2. アサーションは、そのコンポーネントの単体テストと同等の場所で使用してください。アサーションには、デバッグビルド全体で常に検証されるという素晴らしい特性があります。したがって、テストの個別のユニットで「従業員」クラスの制約をテストする代わりに、システム内のすべてのテストケースを通じて従業員クラスを効果的にテストしています。アサーションには、ユニットテスト(最終的にはscaffolding / mocking / whateverが必要)ほどコード量を増加させないという優れた特性もあります。

    誰かが私を殺す前に:本番ビルドはアサーションでクラッシュするべきではありません。代わりに、「エラー」レベルでログを記録する必要があります。

    まだ考えていない人への注意として、ユーザーやネットワークの入力について何も主張しないください。それは大きな間違いです™。

    私の最新のコードベースでは、アサーションの明らかな機会が見られる場合はいつでも、ユニットテストを慎重に削除しています。これにより、全体的なメンテナンスのコストが大幅に削減され、私はずっと幸せになりました。

  3. システム/統合テストを優先し、すべての主要なフローとユーザーエクスペリエンスに対してテストを実装します。コーナーケースはおそらくここにある必要はありません。システムテストでは、すべてのコンポーネントを実行して、ユーザー側での動作を検証します。そのため、システムテストは必然的に遅くなるので、重要なものを(それ以上でもそれ以下でも)書くと、最も重要な問題をキャッチできます。システムテストのメンテナンスオーバーヘッドは非常に低くなっています。

    アサーションを使用しているため、各システムテストは同時に数百の「ユニットテスト」を実行することを覚えておくことが重要です。また、最も重要なものが複数回実行されることをかなり確信しています。

  4. 機能的にテストできる強力なAPIを作成します。APIが機能するコンポーネントを単独で検証することを難しくしている場合、機能テストは厄介であり(実際に直面して)意味がありません。優れたAPI設計により、a)テスト手順が簡単になり、b)明確で価値のある主張が生まれます。

    機能テストは、特にプロセスの障壁を越えて1対多またはさらに悪いことに多対多で通信するコンポーネントがある場合に、正しく実行するのが最も難しいものです。単一のコンポーネントに接続されている入力と出力が多いほど、機能テストは難しくなります。実際にその機能をテストするには、そのうちの1つを分離する必要があるためです。


「単体テストを作成しない」という問題について、例を示します。

TEST(exception_thrown_on_null)
{
    InternalDataStructureType sink;
    ASSERT_THROWS(sink.consumeFrom(NULL), std::logic_error);
    try {
        sink.consumeFrom(NULL);
    } catch (const std::logic_error& e) {
        ASSERT(e.what() == "You must not pass NULL as a parameter!");
    }
}

このテストの作成者は、最終製品の検証にまったく寄与しない7行を追加しました。a)誰もそこにNULLを渡してはならない(そのためアサーションを書く)か、b)NULLケースがいくつかの異なる振る舞いを引き起こすため、ユーザーはこれが起こることを決して見ないはずです。ケースが(b)の場合、実際にその動作を検証するテストを作成します。

私の哲学は、実装成果物をテストするべきではないというものになりました。テストするのは、実際の出力とみなせるもののみです。そうでなければ、単体テスト(特定の実装を強制する)と実装自体の間に2倍の基本的なコードを書くことを避ける方法はありません。

ここで、単体テストに適した候補があることに注意することが重要です。実際、単体テストが、何かを検証するための唯一の適切な手段であり、それらのテストを記述して維持することが非常に価値があるいくつかの状況ですらあります。私の頭の中で、このリストには、自明でないアルゴリズム、APIで公開されたデータコンテナー、および「複雑」に見える高度に最適化されたコード(別名「次の男がおそらくそれを台無しにする」)が含まれています。

あなたへの私の具体的なアドバイスは、次のとおりです。ユニットテストが壊れたら慎重に削除を開始し、「これは出力なのか、コードを無駄にしているのか」という質問を自問します。あなたはおそらくあなたの時間を無駄にしているものの数を減らすことに成功するでしょう。


3
システム/統合テストを優先する-これは驚くほど悪いです。システムは、これらの(sloww!)テストを使用して、ユニットレベルですばやくキャッチできるものをテストし、非常に多くの類似した低速のテストがあるため、それらの実行に数時間かかります。
リッチメルトン

1
@RitchMeltonディスカッションとはまったく別に、新しいCIサーバーが必要なようです。CIはそのように振る舞うべきではありません。
アンドレスジャアンタック

1
クラッシュするプログラム(アサーションが行うこと)は、テストランナー(CI)を殺すべきではありません。そのため、テストランナーがいます。そのため、何かがそのような障害を検出して報告できます。
アンドレスジャアンタック

1
私がよく知っている(テストアサーションではなく)デバッグ専用の「アサート」スタイルのアサーションは、開発者との対話を待っているためCIをハングさせるダイアログをポップアップします。
リッチメルトン

1
ああ、それは私たちの意見の相違について多くを説明するでしょう。:)私はCスタイルのアサーションに言及しています。これは.NETの質問であることに気付いたばかりです。cplusplus.com/reference/clibrary/cassert/assert
Andres Jaan Tack

5

あなたのユニットテストは魅力のように動作するように思えます。それは全体的な点の一種であるため、変更に対して非常に脆弱であることは良いことです。コードブレークテストの小さな変更により、プログラム全体でエラーの可能性を排除できます。

ただし、メソッドが失敗したり、予期しない結果をもたらすような条件についてのみテストする必要があることに注意してください。これにより、些細なことではなく真の問題がある場合、ユニットテストが「壊れる」傾向が高くなります。

あなたはプログラムを大きく再設計しているように思えますが。そのような場合は、必要なことをすべて行い、古いテストを削除して、後で新しいテストに置き換えます。ユニットテストの修復は、プログラムの根本的な変更のために修正しない場合にのみ価値があります。そうしないと、プログラムコードの新しく記述されたセクションに適用するには、テストの書き換えに専念しすぎていることに気付く場合があります。


3

私は他の人がもっと多くのインプットを持っていると確信していますが、私の経験では、これらはあなたを助ける重要なものです:

  1. テストオブジェクトファクトリを使用して入力データ構造を構築します。そのため、そのロジックを複製する必要はありません。おそらく、AutoFixtureなどのヘルパーライブラリを調べて、テストのセットアップに必要なコードを削減してください。
  2. テストクラスごとに、SUTの作成を集中化することで、リファクタリングが行われたときに簡単に変更できます。
  3. テストコードは製品コードと同じくらい重要であることを忘れないでください。また、自分が繰り返していることに気付いた場合、コードが手に負えないと感じた場合など、リファクタリングする必要があります。

テスト間でコードを再利用するほど、脆弱になります。1つのテストを変更すると別のテストが壊れる可能性があるためです。保守性の見返りに、それは合理的なコストかもしれません-私はここでその議論に入らない-しかし、ポイント1と2がテストをより脆弱にしないと主張すること(これは問題でした)は間違っています。
-pdr

@driis-そうです、テストコードにはコードの実行とは異なるイディオムがあります。「一般的な」コードをリファクタリングし、IoCコンテナのようなものを使用して物事を隠すことは、テストで公開されている設計上の問題をマスクするだけです。
リッチメルトン

@pdrが行うポイントは単体テストでは有効である可能性が高いですが、統合/システムテストでは、「タスクXのアプリケーションを準備する」という観点から考えると便利だと思います。これには、適切な場所への移動、特定の実行時設定の設定、データファイルのオープンなどが含まれます。同じ場所で複数の統合テストを開始する場合、そのようなアプローチのリスクと制限を理解していれば、複数のテストでコードをリファクタリングして再利用することは悪いことではないかもしれません。
CVn

2

ソースコードで行うようにテストを処理します。

バージョン管理、チェックポイントのリリース、問題の追跡、「機能の所有権」、計画と労力の見積もりなど。


1

Gerard MeszarosのXUnitテストパターンを必ず見てください。テストコードを再利用し、重複を避けるための多くのレシピを含む素晴らしいセクションがあります。

テストが脆弱な場合は、ダブルをテストするのに十分な手段がないことも考えられます。特に、各ユニットテストの開始時にオブジェクトのグラフ全体を再作成すると、テストのArrangeセクションが大きくなり、かなりの数のテストでArrangeセクションを書き直さなければならない状況に陥ることがよくあります。最もよく使用されるクラスの1つが変更されました。モックとスタブは、関連するテストコンテキストを得るためにリハイドレートする必要があるオブジェクトの数を減らすことで、ここで役立ちます。

モックとスタブを使用してテストセットアップから重要でない詳細を取り出し、テストパターンを適用してコードを再利用すると、脆弱性が大幅に減少します。

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