web-dev-qa-db-ja.com

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

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

問題の例として、実行の一部として他の5つのメソッドを呼び出すメソッドがあるとします。このメソッドのテストは、これら5つのメソッドの1つが呼び出された結果として動作が発生することを確認することです。したがって、単体テストは1つの理由と1つの理由でのみ失敗するはずなので、他の4つのメソッドを呼び出すことによって引き起こされる潜在的な問題を排除し、それらを模擬したいと考えています。すごい!単体テストが実行され、モックされたメソッドが無視され(他の単体テストの一部としてその動作を確認できます)、検証が機能します。

しかし、新しい問題があります-ユニットテストには、howの詳細な知識があり、将来、他の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());
}

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

更新-みんなのフィードバックに感謝します。残念ながら、それは驚くことではありません。テストされているコードが不十分な場合、ユニットテストで実行できる優れたソリューション、パターン、または実践はないようです。この単純な真実を最もよく捉えた答えです。

21
PremiumTier
  1. より適切に設計されるようにコードを修正します。テストにこれらの問題がある場合、変更しようとするとコードの問題が悪化します。

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

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

18
Telastyn

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

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

しかし、これは私の側の異端であることに注意します。ユニットテストに熱心に取り組んでいる人々は、「純粋な」ユニットテストを提唱し、私が説明したことを「統合テスト」と呼んでいます。私はその区別を個人的に心配していません。

8
btilly

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

howをテストしないでくださいwhatをテストします。重要なのは結果です。必要に応じてサブメソッドを含めてください。

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

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

3
Joppe