過度のモッキングが必要なため、脆弱な単体テスト


21

私は、チームで実装しているユニットテストに関して、ますます厄介な問題に取り組んでいます。うまく設計されていないユニットコードをレガシーコードに追加しようとしていますが、実際にテストを追加するのに苦労することはありませんが、テストの結果に苦労し始めています。

問題の例として、実行の一部として5つの他のメソッドを呼び出すメソッドがあるとしましょう。このメソッドのテストは、これらの5つのメソッドのいずれかが呼び出された結果として動作が発生することを確認することです。そのため、単体テストは1つの理由と1つの理由でのみ失敗するはずなので、これらの他の4つのメソッドを呼び出して発生する潜在的な問題を排除し、それらをモックアウトする必要があります。すばらしいです!単体テストが実行され、モックされたメソッドは無視され(その動作は他の単体テストの一部として確認できます)、検証が機能します。

しかし、新しい問題があります-単体テストには、今後、他の4つのメソッドのいずれかに対する動作とシグネチャの変更、または「親メソッド」に追加する必要のある新しいメソッドの確認方法に関する詳細な知識があります。失敗の可能性を回避するために、単体テストを変更する必要があります。

当然のことながら、より多くのメソッドがより少ない動作を達成するようにするだけで、問題を多少軽減できますが、おそらくよりエレガントなソリューションが利用できることを望んでいました。

問題をキャプチャする単体テストの例を次に示します。

簡単なメモとして、「MergeTests」は単体テストクラスであり、テストしているクラスから継承し、必要に応じて動作をオーバーライドします。これは、外部クラス/依存関係への呼び出しをオーバーライドできるようにするためにテストで使用する「パターン」です。

[TestMethod]
public void VerifyMergeStopsSpinner()
{
    var mockViewModel = new Mock<MergeTests> { CallBase = true };
    var mockMergeInfo = new MergeInfo(Mock.Of<IClaim>(), Mock.Of<IClaim>(), It.IsAny<bool>());

    mockViewModel.Setup(m => m.ClaimView).Returns(Mock.Of<IClaimView>);
    mockViewModel.Setup(
        m =>
        m.TryMergeClaims(It.IsAny<Func<bool>>(), It.IsAny<IClaim>(), It.IsAny<IClaim>(), It.IsAny<bool>(),
                         It.IsAny<bool>()));
    mockViewModel.Setup(m => m.GetSourceClaimAndTargetClaimByMergeState(It.IsAny<MergeState>())).Returns(mockMergeInfo);
    mockViewModel.Setup(m => m.SwitchToOverviewTab());
    mockViewModel.Setup(m => m.IncrementSaveRequiredNotification());
    mockViewModel.Setup(m => m.OnValidateAndSaveAll(It.IsAny<object>()));
    mockViewModel.Setup(m => m.ProcessPendingActions(It.IsAny<string>()));

    mockViewModel.Object.OnMerge(It.IsAny<MergeState>());    

    mockViewModel.Verify(mvm => mvm.StopSpinner(), Times.Once());
}

残りの人はどのようにこれに対処しましたか、またはそれを処理する優れた「シンプルな」方法はありませんか?

更新-皆さんのフィードバックに感謝します。残念ながら、実際に驚くことではありません。テスト対象のコードが貧弱であれば、単体テストで従うことのできる優れたソリューション、パターン、または実践はないようです。この単純な真実を最もよく捉えた答えをマークしました。


うわー、模擬セットアップのみが表示され、SUTのインスタンス化などはありません。ここで実際の実装をテストしていますか?StopSpinnerを呼び出すのは誰ですか?OnMerge?あなたはそれが事自体を呼び出すないかもしれませんが、任意の依存関係を模擬する必要があり...
Joppe

見づらいですが、Mock <MergeTests>はSUTです。CallBaseフラグを設定して、「OnMerge」メソッドが実際のオブジェクトで実行されるようにしますが、依存関係の問題などによりテストが失敗する可能性のある「OnMerge」によって呼び出されるメソッドをモックアウトします。テストの目的は最後の行です-この場合、スピナーを停止したことを確認します。
PremiumTier

MergeTestsは別のインスツルメントされたクラスのように聞こえますが、本番環境に存在するものではないため、混乱しています。
ジョッペ


1
他の問題とは別に、SUTがMock <MergeTests>であるというのは間違っているようです。なぜモックをテストするのですか?なぜMergeTestsクラス自体をテストしないのですか?
エリックキング

回答:


18
  1. コードを修正して、設計を改善します。テストにこれらの問題がある場合、コードを変更しようとすると、コードにさらに問題が発生します。

  2. それができない場合は、おそらくあまり理想的でないことが必要です。メソッドの事前条件と事後条件に対してテストします。他の5つの方法を使用している場合、誰が気にしますか?彼らはおそらく、テストが失敗したときに失敗の原因を明確にする(独自の)独自のユニットテストを持っています。

「単体テストに失敗する理由は1つしかありません」は良いガイドラインですが、私の経験では非現実的です。書くのが難しいテストは書かれません。脆弱なテストは信じられません。


私はコードの設計を修正することに完全に同意しますが、厳しいスケジュールで大企業向けに開発するという理想的ではない世界では、過去のチームや下手な決定によって生じた技術的負債を「完済」することを主張するのは難しいかもしれません一度。あなたの2番目の点まで、モックの多くは、1つの理由でテストが失敗するだけではありません-それは、そのコード内で作成された多数の依存関係を最初に処理せずに実行中のコードを実行できないためです。目標の投稿を移動してすみません。
PremiumTier

より良い設計が現実的でない場合、「他の5つの方法を使用している場合、誰が気にしますか?」メソッドがその方法ではなく、必要な機能を実行することを確認します。
Kwebble

@Kwebble-しかし、質問の目的は、テストを実行するためにメソッド内で呼び出される他の動作をモックアウトする必要があるときに、メソッドの動作を検証する簡単な方法があるかどうかを判断することでした。「方法」を削除したいのですが、方法がわかりません:)
PremiumTier

魔法の特効薬はありません。貧弱なコードをテストする「簡単な方法」はありません。テスト対象のコードをリファクタリングする必要があるか、テストコード自体も不良です。あなたに実行したり、などしているとして、それは、内部の詳細にあまりにも固有のものになりますので、テストが悪くなるのいずれかbtilly提案し、あなたが作業環境に対してテストを実行することができますが、その後のテストははるかに遅く、より複雑になります。いずれにせよ、テストは書くのが難しく、維持するのが難しく、偽陰性になりやすいでしょう。
スティーブン・ドッグガート

8

大きなメソッドをより焦点の合った小さなメソッドに分割することは、間違いなくベストプラクティスです。単体テストの動作を確認するのは苦痛ですが、他の方法でも苦痛を経験しています。

とはいえ、それは異端ですが、私は個人的に現実的な一時的なテスト環境を作成するのが好きです。つまり、他のメソッドの内部に隠されているすべてのものをモックアウトするのではなく、それらすべてを実行できる一時環境(プライベートデータベースとスキーマを備えた完全な-SQLiteが役立つ場合があります)を簡単にセットアップできることを確認してください。そのテスト環境をどのように構築/分解するかを知る責任は、それを必要とするコードとともに存在するため、その変更時に、その存在に依存する単体テストコードをすべて変更する必要はありません。

しかし、これは私の側の異端であることに注意してください。ユニットテストに熱心な人は、「純粋な」ユニットテストを支持し、私が説明したものを「統合テスト」と呼びます。私は個人的にその区別について心配しません。


3

モックの緩和を検討し、それが呼び出すメソッドを含む可能性のあるテストを作成するだけです。

方法をテストしないで、何をテストします。重要なのは結果であり、必要に応じてサブメソッドを含めます。

別の角度から、テストを定式化し、1つの大きなメソッドで合格させ、リファクタリングして、リファクタリング後にメソッドツリーを作成できます。それらを個別にテストする必要はありません。重要なのは最終結果です。

サブメソッドがいくつかの側面をテストするのを難しくする場合、テスト対象のクラスを大きくインストルメント/シームせずにそれらをよりきれいにモックできるように、それらを別々のクラスに分割することを検討してください。サンプルテストで具体的な実装を実際にテストしているかどうかを判断するのは、ちょっと難しいです。


問題は、「何」をテストする「方法」をモック化する必要があることです。これは、コードの設計によって課される制限です。それがテストをもろくするものであるので、私は確かに「モック」したくない。
PremiumTier

メソッド名を見ると、テストされたクラスが単純に多くの責任を負っていると思います。単一責任の原則を読んでください。MVCからの借用は少し役立つかもしれませんが、クラスはUI、インフラストラクチャ、およびビジネスの両方の問題を処理しているようです。
ジョッペ

。うん:(それは我々が再設計とリファクタリングに取り組んでいる私が言及していたことが不十分に設計されたレガシーコードをだろうが、我々はそれが最初のテストの下のソースを置くのがベストだろうと感じた。
PremiumTier
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.