すべての単体テストでデータをハードコーディングする必要がありますか?


33

ほとんどの単体テストのチュートリアル/例では、通常、個々のテストごとにテストするデータを定義する必要があります。これは「すべてを単独でテストする必要がある」理論の一部だと思います。

しかし、大量のDIを含む多層アプリケーションを扱う場合、各テストのセットアップに必要なコードが非常に長くかかることがわかりました。代わりに、多くのテストスキャフォールドが事前に構築された多数のテストベースクラスを構築し、継承することができます。

この一環として、実行中のアプリケーションのデータベースを表す偽のデータセットも作成していますが、通常、各「テーブル」には1行または2行しかありません。

すべてではないにしても、すべての単体テストのテストデータの大部分を事前に定義することは、受け入れられている慣行ですか?

更新

以下のコメントから、単体テストよりも統合を進めているように感じます。

私の現在のプロジェクトはASP.NET MVCで、Entity Framework Code Firstを使用した作業単位とテスト用のMoqを使用しています。UoWとリポジトリをモックしましたが、実際のビジネスロジッククラスを使用し、コントローラーアクションをテストしています。テストでは、UoWがコミットされていることを頻繁に確認します。たとえば、次のとおりです。

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBase模擬UoWを構築し、をインスタンス化していuserLogicます。

多くのテストでは、データベースに既存のユーザーまたは製品が必要です。そのため、この例userDataでは、IList<User>ユーザーレコードが1つだけである模擬UoWが返すものを事前に設定しました。


4
チュートリアル/例の問題は、単純である必要があることですが、単純な例では複雑な問題の解決策を示すことはできません。適切なサイズの実際のプロジェクトでツールがどのように使用されるかを説明する「ケーススタディ」を添付する必要がありますが、ほとんど使用されません。
ジャン・ヒューデック

たぶんあなたが持っているコードのいくつかの小さな例を追加することができます。
リュックフランケン

テストを実行するために多くのセットアップコードが必要な場合は、機能テストを実行するリスクがあります。コードを変更してもテストに失敗しても、コードに問題がない場合。これは間違いなく機能テストです。
-Reactgular

「xUnit Test Patterns」という本は、再利用可能なフィクスチャとヘルパーの強力な事例となっています。テストコードは、他のコードと同様に保守可能でなければなりません。
チャッククルチンガー

この記事は役に立つかもしれません:yegor256.com/2015/05/25/unit-test-scaffolding.html
yegor256

回答:


25

最終的には、できるだけ多くの結果を得るために、できるだけ少ないコードを書きたいと思います。複数のテストで多くの同じコードを使用すると、a)コピーアンドペーストコーディングが発生する傾向があり、b)メソッドシグネチャが変更された場合、多くの壊れたテストを修正しなければならなくなる可能性があります。

私は日常的に使用する多くのデータ型を提供する標準のTestHelperクラスを持つアプローチを使用しているため、テストのために標準エンティティまたはDTOクラスのセットを作成して、毎回何を取得するかを正確に把握できます。したがってTestHelper.GetFooRange( 0, 100 )、すべての依存クラス/フィールドが設定された100個のFooオブジェクトの範囲を取得するために呼び出すことができます。

特に、物事を正しく実行するために存在する必要があるが、多くの時間を節約できるこのテストにとって必ずしも重要ではないORM型システムで構成された複雑な関係がある場合。

データレベルの近くでテストする状況では、同様の方法でクエリできるリポジトリクラスのテストバージョンを作成することがあります(これもORMタイプの環境であり、実際のデータベース)、クエリへの正確な応答をモックアウトすることは多くの作業であり、多くの場合わずかな利点しか提供しないためです。

単体テストでは注意が必要なことがいくつかあります。

  • モックモックであることを確認してください。ユニットテストを行う場合、テスト対象のクラスの周りで操作を実行するクラスはモックオブジェクトでなければなりません。DTO /エンティティタイプのクラスは本物になり得ますが、クラスが操作を実行している場合は、それらをモックする必要があります-さもなければ、サポートコードが変更され、テストが失敗し始めた場合、どの変更を見つけるためにもっと長く検索する必要があります実際に問題を引き起こしました。
  • クラスをテストしていることを確認してください。場合によっては、単体テストのスイートを調べると、テストの半分が、テスト対象の実際のコードよりも実際にモックフレームワークをテストしていることが明らかになります。
  • モック/サポートオブジェクトを再利用しないでくださいこれは大したことです-ユニットテストをサポートするコードで巧妙になろうとすると、予期しない効果をもたらす可能性のあるテスト間で永続するオブジェクトを不注意に作成するのは本当に簡単です。たとえば、昨日は、単独で実行すると合格し、クラス内のすべてのテストが実行されると合格しましたが、テストスイート全体が実行されると失敗するテストがありました。テストヘルパーでは、私が作成したときに問題を引き起こすことはなかったはずの卑劣な静的オブジェクトが存在していました覚えておいてください:テストの開始時にすべてが作成され、テストの終了時にすべてが破棄されます。

10

テストの意図をより読みやすくするものは何でも。

一般的な経験則として:

データがテストの一部である場合(たとえば、状態7の行を印刷しないでください)、テストでコーディングして、作成者が意図したことを明確にします。

データが処理するものがあることを確認するための単なるフィラーである場合(例:処理サービスが例外をスローする場合、レコードを完了としてマークしない)、すべての方法で、BuildDummyDataメソッドまたは無関係なデータをテストから除外するテストクラスを使用する。

しかし、後者の良い例を考えるのに苦労していることに注意してください。単体テストフィクスチャにこれらの多くがある場合、解決すべき別の問題がある可能性があります...テスト対象のメソッドが複雑すぎる可能性があります。


+1同意します。これは、彼がテストしているのは、単体テストのために密結合することのような匂いです。
Reactgular

5

さまざまなテスト方法

まず、ユニットテストまたは統合テストを定義します。レイヤーの数は、1つのクラスのみをテストする可能性が高いため、ユニットテストには関係ありません。あなたがモックアウトする残り。統合テストの場合、複数のレイヤーをテストすることは避けられません。適切な単体テストがある場合は、統合テストを複雑にしすぎないようにすることがコツです。

単体テストが適切であれば、統合テストを行うときにすべての詳細をテストする必要はありません。

私たちが使用する用語は、それらはプラットフォームに少し依存していますが、ほとんどすべてのテスト/開発プラットフォームで見つけることができます:

応用例

使用するテクノロジーによって名前は異なる場合がありますが、例としてこれを使用します。

モデルProduct、ProductsController、および製品を含むHTMLテーブルを生成するインデックスビューを持つ単純なCRUDアプリケーションがある場合:

アプリケーションの最終結果は、アクティブなすべての製品のリストを含むHTMLテーブルを示しています。

単体テスト

モデル

モデルは非常に簡単にテストできます。それにはさまざまな方法があります。フィクスチャを使用します。それはあなたが「偽のデータセット」と呼ぶものだと思います。したがって、各テストを実行する前に、テーブルを作成し、元のデータを入れます。ほとんどのプラットフォームには、このためのメソッドがあります。たとえば、テストクラスでは、各テストの前に実行されるメソッドsetUp()。

次に、テストを実行します(例:testGetAllActive製品)。

したがって、テストデータベースに対して直接テストします。データソースのモックアウトは行いません。常に同じにします。これにより、たとえばデータベースの新しいバージョンでテストでき、クエリの問題が発生します。

現実の世界では、常に100%の単一の責任に従うことはできません。これをさらに良くしたい場合は、モックしたデータソースを使用できます。私たち(ORMを使用)にとっては、既存のテクノロジーをテストするような感覚です。また、テストははるかに複雑になり、クエリを実際にテストしません。そのため、このようにします。

ハードコードされたデータは、フィクスチャーに個別に保存されます。したがって、フィクスチャはcreate tableステートメントと使用するレコードの挿入を含むSQLファイルのようなものです。大量のレコードでテストする必要が実際にない限り、それらを小さく保ちます。

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

コントローラ

コントローラーを使用してモデルをテストしたくないため、コントローラーにはさらに作業が必要です。したがって、モデルのモックを作成します。つまり、レコードのリストを返すindex()メソッドをテストします。

そこで、モデルメソッドgetAllActive()をモックアウトし、その中に固定データ(たとえば2つのレコード)を追加します。次に、コントローラーがビューに送信するデータをテストし、実際にこれらの2つのレコードを取得するかどうかを比較します。

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

それで十分です。テストを難しくするため、コントローラーに機能をほとんど追加しないようにします。しかしもちろん、常にいくつかのコードが含まれています。たとえば、次のような要件をテストします。ログインしている場合にのみ、これらの2つのレコードを表示します。

そのため、コントローラーには通常1つのモックとハードコードされた小さなデータが必要です。ログインシステムの場合は、別のシステムになる可能性があります。テストでは、ヘルパーメソッドsetLoggedIn()があります。これにより、ログインありまたはログインなしのテストが簡単になります。

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

視聴回数

ビューのテストは困難です。最初に、繰り返すロジックを分離します。ヘルパーに入れて、それらのクラスを厳密にテストします。常に同じ出力が期待されます。たとえば、generateHtmlTableFromArray()。

次に、プロジェクト固有のビューがいくつかあります。私たちはそれらをテストしません。それらを単体テストすることは本当に望ましくありません。統合テストのためにそれらを保持します。多くのコードをビューに組み込んだため、ここでのリスクは低くなります。

これらのテストを開始する場合、ほとんどのプロジェクトでは役に立たないHTMLを変更するたびにテストを変更する必要があります。

echo $this->tableHelper->generateHtmlTableFromArray($products);

統合テスト

ここにあなたのプラットフォームによっては、それは次のようにウェブベースの可能など、ユーザーストーリー、と働くことができるセレンまたは他の同等のソリューション。

通常、フィクスチャを使用してデータベースをロードし、利用可能なデータをアサートします。完全な統合テストでは、通常、非常にグローバルな要件を使用します。そのため、製品をアクティブに設定し、製品が使用可能になるかどうかを確認します。

適切なフィールドが使用可能かどうかなど、すべてを再度テストするわけではありません。ここで、より大きな要件をテストします。コントローラーまたはビューからテストを複製したくないので。何かが本当にアプリケーションのキー/コア部分である場合、またはセキュリティ上の理由(パスワードを確認できない)である場合は、正しいことを確認するためにそれらを追加します。

ハードコードされたデータはフィクスチャに保存されます。

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}

これは、まったく異なる質問に対する素晴らしい答えです。
-pdr

フィードバックをお寄せいただきありがとうございます。あなたは私がそれをあまりにも具体的に言及しなかったことは正しいかもしれません。詳細な回答の理由は、質問でテストするときに最も難しいことの1つを見るからです。単独でのテストがさまざまな種類のテストにどのように適合するかの概要。そのため、データの処理方法(または分離方法)をすべての部分に追加しました。私はそれをより明確にすることができるかどうかを確認してみましょう。
リュックフランケン

答えは、他のすべての種類のクラスを呼び出さずにテストする方法を説明するために、いくつかのコード例で更新されました。
リュックフランケン

4

「実際の」データソースを使用するまで、多くのDIと配線を伴うテストを作成している場合、おそらく単純な単体テストの領域を離れ、統合テストの領域に入りました。

統合テストの場合、共通のデータ設定ロジックを持つことは悪い考えではないと思います。このようなテストの主な目的は、すべてが正しく構成されていることを証明することです。これは、システムを介して送信される具体的なデータとはかなり無関係です。

一方、ユニットテストの場合、テストクラスのターゲットを単一の「実際の」クラスに保ち、他のすべてをモックすることをお勧めします。次に、テストデータを実際にハードコーディングして、できるだけ多くの特別な/前のバグパスをカバーするようにします。

テストに半ハードコーディング/ランダム要素を追加するには、ランダムモデルファクトリを導入します。モデルのインスタンスを使用したテストでは、これらのファクトリーを使用して、有効であるが完全にランダムなモデルオブジェクトを作成し、手近なテストに必要なプロパティのみをハードコーディングします。このようにして、テストですべての関連データを直接指定すると同時に、すべての無関係なデータを指定し、(ある程度)他のモデルフィールドに意図しない依存関係がないことをテストする必要もありません。


-1

テストのためにほとんどのデータをハードコーディングすることはかなり一般的だと思います。

特定のデータセットによってバグが発生するという単純な状況を考えてみましょう。具体的には、そのデータの単体テストを作成して修正を実行し、バグが再発しないことを確認します。時間が経つにつれて、テストには多くのテストケースをカバーする一連のデータが含まれるようになります。

また、事前定義されたテストデータを使用すると、広く知られているさまざまな状況をカバーする一連のデータを構築できます。

そうは言っても、テストにランダムなデータを含めることには価値があると思います。


タイトルだけでなく質問を本当に読みましたか?
ヤコブ

テストでランダムなデータを使用することの価値 -ええ、テストが毎週1回失敗したときに何が起こったのかを把握しようとするのとまったく異なります。
pdr

ヘイズ/ファジング/入力テストのテストでランダムデータを使用することには価値があります。しかし、単体テストではそうではなく、それは悪夢です。
グレナトロン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.