web-dev-qa-db-ja.com

大きくて複雑なクラスでユニットテストを実装するにはどうすればよいですか?

私は、いくつかの計算を含む財務システムで単体テストを実装しています。メソッドの1つは、100を超えるプロパティを持つパラメーターごとにオブジェクトを受け取り、このオブジェクトのプロパティに基づいて、戻り値を計算します。このメソッドの単体テストを実装するには、このオブジェクトすべてに有効な値を入力する必要があります。

だから...質問:今日、このオブジェクトはデータベースを介して入力されています。私の単体テスト(私はNUnitを使用しています)では、データベースを回避し、そのモックオブジェクトを作成して、メソッドの戻り値のみをテストする必要があります。この巨大なオブジェクトでこのメソッドを効率的にテストするにはどうすればよいですか?本当に100個のプロパティすべてを手動で入力する必要がありますか?たとえば、Moqを使用してこのオブジェクトの作成を自動化する方法はありますか?

obs:すでに作成されているシステムの単体テストを書いています。現時点では、すべてのアーキテクチャを書き直すことは不可能です。
どうもありがとう!

18
Lisa Shiphrah

これらの100個の値が関連性がなく、そのうちのいくつかだけが必要な場合は、いくつかのオプションがあります。

新しいオブジェクトを作成し(プロパティは、文字列の場合はnull、整数の場合は0などのデフォルト値で初期化されます)、必要なプロパティのみを割り当てることができます。

 var obj = new HugeObject();
 obj.Foo = 42;
 obj.Bar = "banana";

AutoFixture のようなライブラリを使用して、オブジェクト内のすべてのプロパティにダミー値を割り当てることもできます。

 var fixture = new Fixture();
 var obj = fixture.Create<HugeObject>();

必要なプロパティを手動で割り当てるか、フィクスチャビルダーを使用できます

 var obj = fixture.Build<HugeObject>()
                  .With(o => o.Foo, 42)
                  .With(o => o.Bar, "banana")
                  .Create();

同じ目的のための別の有用なライブラリは NBuilder


注:すべてのプロパティがテストしている機能に関連していて、特定の値が必要な場合、テストに必要な値を推測するライブラリはありません。唯一の方法は、テスト値を手動で指定することです。ただし、各テストの前にいくつかのデフォルト値を設定し、特定のテストに必要なものを変更するだけで、多くの作業を省くことができます。つまり事前定義された値のセットでオブジェクトを作成するヘルパーメソッドを作成します。

 private HugeObject CreateValidInvoice()
 {
     return new HugeObject {
         Foo = 42,
         Bar = "banaba",
         //...
     };
 }

そして、テストでは、いくつかのフィールドをオーバーライドするだけです。

 var obj = CreateValidInvoice();
 obj.Bar = "Apple";
 // ...
16

テストのために実際に正しいデータを大量に取得する必要がある場合のために、データをJSONにシリアル化し、それをテストクラスに直接配置しました。元のデータをデータベースから取得して、シリアル化することができます。このようなもの:

[Test]
public void MyTest()
{
    // Arrange
    var data = GetData();

    // Act
    ... test your stuff

    // Assert
    .. verify your results
}


public MyBigViewModel GetData()
{
    return JsonConvert.DeserializeObject<MyBigViewModel>(Data);
}

public const String Data = @"
{
    'SelectedOcc': [29, 26, 27, 2,  1,  28],
    'PossibleOcc': null,
    'SelectedCat': [6,  2,  5,  7,  4,  1,  3,  8],
    'PossibleCat': null,
    'ModelName': 'c',
    'ColumnsHeader': 'Header',
    'RowsHeader': 'Rows'
    // etc. etc.
}";

このようなテストが多数ある場合、この形式のデータを取得するにはかなりの時間がかかるため、これは最適ではない可能性があります。ただし、これにより、シリアル化が完了した後でさまざまなテスト用に変更できる基本データが得られます。

このJSONを取得するには、データベースでこの大きなオブジェクトを個別にクエリし、JsonConvert.Serialiseを介してJSONにシリアル化し、この文字列をソースコードに記録する必要があります。このビットは比較的簡単ですが、必要なため時間がかかります。手動で行うには... 1回だけです。

レポートのレンダリングをテストする必要があり、DBからデータを取得することが現在のテストの問題ではなかったときに、この手法を正常に使用しました。

p.s. Newtonsoft.Jsonを使用するには、JsonConvert.DeserializeObjectパッケージが必要です。

5
trailmax

制限(悪いコード設計と技術的負債...私は子供)を考えると、単体テストは手動で入力するのが非常に面倒です。ハイブリッド統合テストは、実際のデータソース(本番環境のデータソースではない)にアクセスする必要がある場合に必要になります。

潜在的なポーション

  1. データベースのコピーを作成し、依存する複合クラスにデータを入力するために必要なテーブル/データのみにデータを入力します。うまくいけば、コードは十分にモジュール化されているので、データアクセスは複雑なクラスを取得してデータを取り込むことができます。

  2. データアクセスをモックし、代替ソースを介して必要なデータをインポートします(フラットファイルかもしれませんか?csv)

他のすべてのコードは、単体テストの実行に必要な他の依存関係のモックに焦点を当てることができます。

残っている他の唯一のオプションがクラスに手動でデータを入力することであることを除けば。

余談ですが、これは全体的に悪いコードの臭いがありますが、現時点では変更できないことを考えると、OPの範囲外です。意思決定者にこれについて言及することをお勧めします。

5
Nkosi

まず最初に、このオブジェクトのacquisitionを、現在DBからプルしているコードの場合は、インターフェイスを介して実行する必要があります。次に、そのインターフェイスをモックして、単体テストで必要なものを返すことができます。

もし私があなたの立場にあったら、実際のcalculationロジックを抽出し、その新しい「計算機」クラスに向けてテストを書きます。できる限りすべてを分解します。入力に100個のプロパティがあるが、すべてが各計算に関連しているわけではない場合は、interfacesを使用してそれらを分割します。これにより、期待される入力が表示され、コードも改善されます。

したがって、あなたの場合、クラスにBigClassという名前を付けると、特定の計算で使用されるインターフェイスを作成できます。このようにして、既存のクラスや他のコードがそれを処理する方法を変更することはありません。抽出された計算機ロジックは独立していて、テスト可能であり、コードははるかに単純です。

    public class BigClass : ISet1
    {
        public string Prop1 { get; set; }
        public string Prop2 { get; set; }
        public string Prop3 { get; set; }
    }

    public interface ISet1
    {
        string Prop1 { get; set; }

        string Prop2 { get; set; }
    }

    public interface ICalculator
    {
        CalculationResult Calculate(ISet1 input)
    }
1
Stefan Georgiev

私はこのアプローチを取ります:

1-100個のプロパティ入力パラメーターオブジェクトのすべての組み合わせに対して単体テストを記述し、ツールを使用してこれを実行し(pex、intellitestなど)、すべてが緑色で実行されることを確認します。この時点で、後で明らかになる理由から、単体テストを単体テストではなく統合テストと呼びます。

2-テストをSOLIDコードのチャンクにリファクタリングします-他のメソッドを呼び出さないメソッドは、他のコードに依存しないため、真にユニットテスト可能と見なすことができます。残りのメソッドはまだ統合のみです。テスト可能。

3-すべての統合テストがまだ緑色で実行されていることを確認します。

4-新しくユニットテスト可能なコードの新しいユニットテストを作成します。

5-すべてが緑色で実行されているので、余分な元の統合テストのすべて/一部を削除できます-そうすることに抵抗がない場合に限り、あなた次第です。

6-すべてが緑色で実行されているので、単体テストに必要な100個のプロパティを、個々のメソッドに厳密に必要なものだけに減らすことができます。これにより、追加のリファクタリングの領域が強調表示される可能性がありますが、とにかくパラメータオブジェクトが単純化されます。これにより、将来のコードメンテナの作業のバグが少なくなり、50個のプロパティがあったときにパラメータオブジェクトのサイズに対処できなかったことが、現在100になっている理由だと思います。この問題に対処できないと、それが意味します。最終的には150個のパラメーターに成長しますが、これに直面することはできません。誰も望んでいません。

1
Greg Trevellick

単体テストにインメモリデータベースを使用する

つまり...ユニットテストと言ったように、これは技術的には答えではありません。インメモリデータベースを使用すると、ユニットテストではなく統合テストになります。しかし、不可能な制約に直面したとき、どこかに与える必要があることがあり、これはそのような場合の1つである可能性があります。

私の提案は、ユニットテストでSQLite(または同様のもの)を使用することです。実際のデータベースを抽出してSQLiteデータベースに複製するツールがあります。その後、スクリプトを生成して、データベースのメモリ内バージョンにロードできます。依存性注入とリポジトリパターンを使用して、「単体」テストと実際のコードで異なるデータベースプロバイダーを設定できます。

このようにして、既存のデータを使用し、必要に応じてテストの前提条件として変更することができます。これは真の単体テストではないことを認める必要があります...つまり、データベースが実際に生成できるものに制限されているため(つまり、テーブルの制約により特定のシナリオのテストが妨げられます)、その意味で完全な単体テストを実行することはできません。また、これらのテストは実際にデータベース作業を行っているため、実行速度が遅くなります。したがって、これらのテストの実行に必要な余分な時間を計画する必要があります。 (通常はまだかなり高速ですが)他のエンティティをモックアウトできることに注意してください(たとえば、データベースに加えてサービスコールがある場合、それはまだモックの可能性があります)。

このアプローチが役立つと思われる場合は、次のリンクを参考にしてください。

SQL ServerからSQLiteへのコンバーター:

https://www.codeproject.com/Articles/26932/Convert-SQL-Server-DB-to-SQLite-DB

SQLiteスタジオ: https://sqlitestudio.pl/index.rvt

(これを使用して、メモリ内で使用するスクリプトを生成します)

メモリで使用するには、次のようにします。

TestConnection = new SQLiteConnection( "FullUri = file :: memory:?cache = shared");

データベース構造用にデータロードとは別のスクリプトがありますが、それは個人的な好みです。

それがお役に立てば幸いです。

0
Reginald Blue