総合的なユニットテストの作成

14296 ワード

各用例の1~2つの断言を作成することは、ユニットテストのベストプラクティスの一般的な内容である.このように考えているのは、ユニットテストを1つしか見せていない人です.そのため、彼らの提案を受け入れると、小さな演算のために品質を保証するために大量のユニットテストが必要です.この文章は例を通じて,1つのテスト用例の複数の断言が必要で価値があることを示すことを意図している.
Personというオブジェクトはデータバインドシーンでよく見られますが、見てみましょう.
 

FirstNameのテスト


最初にFirstNameというプロパティの設定をテストします.

[TestMethod]
public void Person_FirstName_Set()
{
      varperson = new Person("Adam", "Smith");
      person.FirstName = "Bob";
      Assert.AreEqual("Bob", person.FirstName);
}

次にFirstNameの変更通知をテストします.

[TestMethod]
public void Person_FirstName_Set_PropertyChanged()
{
      var person = new Person("Adam", "Smith");
      var eventAssert = new Granite.Testing.PropertyChangedEventAssert(person);
      person.FirstName = "Bob";
      eventAssert.Expect("FirstName");
}

このテストを実行すると、「所望の属性名『FirstName』が受信されたが、『IsChanged』が受信された」という失敗したプロンプトが表示されます.FirstNameのプロパティを設定すると、IsChangedタグがトリガーされることは明らかです.これを考慮する必要があります.そこで私たちはそれを加えました

[TestMethod]
public void Person_FirstName_Set_PropertyChanged()
{
      var person = new Person("Adam", "Smith");
      var eventAssert = new Granite.Testing.PropertyChangedEventAssert(person);
      person.FirstName = "Bob";
      eventAssert.SkipEvent(); //this was IsChanged
      eventAssert.Expect("FirstName");
}

以上の2つのテストを考慮して、FirstNameが変更されたときに他にどのような属性が変更されるかを考えます.APIを表示すると、IsChangedとFullNameのプロパティが変わります.
[TestMethod]
public void Person_FullName_Changed_By_Setting_FirstName()
{
      var person = new Person("Adam", "Smith");
      person.FirstName = "Bob";
      Assert.AreEqual("Bob Smith", person.FullName);
}

[TestMethod]
public void Person_IsChanged_Changed_By_Setting_FirstName()
{
      var person = new Person("Adam", "Smith");
      person.FirstName = "Bob";
      Assert.IsTrue(person.IsChanged);
}

 
もちろん、これらのプロパティが変更された場合は、プロパティ変更通知を取得する必要があります.
[TestMethod]
public void Person_IsChanged_Property_Change_Notification_By_Setting_FirstName()
{
      var person = new Person("Adam", "Smith");
      var eventAssert = new PropertyChangedEventAssert(person);
      person.FirstName = "Bob";
      eventAssert.Expect("IsChanged");
}
[TestMethod]
public void Person_FullName_Property_Change_Notification_By_Setting_FirstName()
{
      var person = new Person("Adam", "Smith");
      var eventAssert = new PropertyChangedEventAssert(person);
      person.FirstName = "Bob";
      eventAssert.SkipEvent(); //this was IsChanged
      eventAssert.SkipEvent(); //this was FirstName
      eventAssert.Expect("FullName");
}

次の2つの試験はHasErrorsという属性とErrorsChangedイベントについて行った.
[TestMethod]
public void Person_FirstName_Set_HasErrorsIsFalse()
{
          var person = new Person("Adam", "Smith");
          person.FirstName = "Bob";
          Assert.IsFalse(person.HasErrors);
}
[TestMethod]
public void Person_FirstName_Set_ErrorsChanged_Did_Not_Fire()
{
          var person = new Person("Adam", "Smith");
          var errorsChangedAssert = new ErrorsChangedEventAssert(person);
          person.FirstName = "Bob";
          errorsChangedAssert.ExpectNothing();
}

現在、8つのテストがあります.これは、FirstNameのプロパティ値を変更すると、変更されるすべてのことを考慮することを意味します.しかし、これは終わりではありません.他に意外な変更がないことを確認する必要があります.理論的には,これはより多くの断言とかなりの数の試験を意味するが,次にHasErrors試験の代わりにChangeAssert法を用いた巧みな方法を採用する.
[TestMethod]
public void Person_FirstName_Set_Nothing_Unexpected_Changed()
{
     var person = new Person("Adam", "Smith");
     var changeAssert = new ChangeAssert(person);
     person.FirstName = "Bob";
     changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged");
}

ChangeAssertはマッピングによってオブジェクトの状態を簡単に取得するので、後で指摘したいくつかの具体的な属性以外は変わらないと断言できます.
おめでとうございます.最初のテスト例を完成しました.一つ完成して、まだたくさん待っています.

なぜ「1つ」のテスト例と言いますか?


その8つのテストは、FirstNameプロパティを上書きして「Adam」から「Bob」に変更するシーンを完了しただけで、他の値がエラー状態、LastNameがnullまたは空でない場合は完了しません.テスト例の完全なリストを見てみましょう.
  • FirstName値を「Adam」
  • に設定
  • FirstName値をnull
  • に設定
  • FirstNameを空白にする
  • LastName値nullの場合case 1-3
  • を実行
  • LastNameが空白の場合、case 1-3
  • を実行します.
  • FirstName値がnullで始まる場合、case 1-5
  • が実行されます.
  • FirstName値が空白で始まる場合、case 1-5
  • が実行されます.
    現在、27の異なるシーンを見ています.各シーンに8つの異なるテストが必要で、このプロパティのみの場合、最大216のテストを実行する必要があります.このような考え方によれば、これはかなり些細なコードです.どうすればいいのでしょうか?

    テストもコードの味がします


    最初のテスト例の8つのテストを振り返ってみると、同じ設定と演算があります.唯一の違いは私たちが書いた断言です.業界ではこれをコード味と呼んでいます.実際、ウィキペディアに記載されているここには2つのコードの味があるはずです.
  • Duplicated code
  • 重複コード
  • Excessively long identifiers
  • 長すぎる識別子
  • 断言をテストに統合することで、この2つのコードの味を簡単に解消することができます.
    
    [TestMethod]
    public void Person_FirstName_Set()
    {
         var person = new Person("Adam", "Smith");
         var eventAssert = new PropertyChangedEventAssert(person);
         var errorsChangedAssert = new ErrorsChangedEventAssert(person);
         var changeAssert = new ChangeAssert(person);
         person.FirstName = "Bob";
         Assert.AreEqual("Bob", person.FirstName, "FirstName setter failed");
         Assert.AreEqual("Bob Smith", person.FullName, "FullName not updated with FirstName changed");
         Assert.IsTrue(person.IsChanged, "IsChanged flag was not set when FirstName changed");
         eventAssert.Expect("IsChanged");
         eventAssert.Expect("FirstName");
         eventAssert.Expect("FullName");
         errorsChangedAssert.ExpectNothing("Expected no ErrorsChanged events");
         changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged");
    } 

    テストに失敗する原因を知ることが重要なので、断言に失敗した情報ヒントを追加します. 

    ユニットテストとコード再利用


    27のテスト例を見てみると、FirstNameをnullまたは空白列に設定するにも同じテストが必要であると判断できます.これにより、次のように拡張できます.
    
    [TestMethod]
    public void Person_FirstName_Set_Empty()
    {
         Person_FirstName_Set_Invalid(String.Empty);
    }
    [TestMethod]
    public void Person_FirstName_Set_Null()
    {
         Person_FirstName_Set_Invalid(null);
    }
    public void Person_FirstName_Set_Invalid(string firstName)
    {
         var person = new Person("Adam", "Smith");
         var eventAssert = new PropertyChangedEventAssert(person);
         var errorsChangedAssert = new ErrorsChangedEventAssert(person);
         var changeAssert = new ChangeAssert(person);
         Assert.IsFalse(person.IsChanged, "Test setup failed, IsChanged is not false");
         Assert.AreEqual("Adam", person.FirstName, "Test setup failed, FirstName is not Adam");
         Assert.AreEqual("Smith", person.LastName, "Test setup failed, LastName is not Smith");
         person.FirstName = firstName;
         Assert.AreEqual(firstName , person.FirstName, "FirstName setter failed");
         Assert.AreEqual("Smith", person.FullName, "FullName not updated with FirstName changed");
         Assert.IsTrue(person.IsChanged, "IsChanged flag was not set when FirstName changed");
         eventAssert.Expect("IsChanged");
         eventAssert.Expect("FirstName");
         eventAssert.Expect("FullName");
         Assert.IsTrue(person.HasErrors, "HasErrors should have remained false");
         errorsChangedAssert.ExpectCountEquals(1, "Expected an ErrorsChanged event");
         changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged", "HasErrors");
    } 

    Personを見つけることができますFirstName_SetとPerson_FirstName_Set_Invalidの違いは小さく、さらに汎用化してみましょう.
    [TestMethod]
    public void Person_FirstName_Set_Valid()
    {
         Person_FirstName_Set("Bob", false);
    }
    [TestMethod]
    public void Person_FirstName_Set_Empty()
    {
         Person_FirstName_Set(String.Empty, true);
    }
    [TestMethod]
    public void Person_FirstName_Set_Null()
    {
         Person_FirstName_Set(null, true);
    }
    public void Person_FirstName_Set(string firstName, bool shouldHaveErrors)
    {
         var person = new Person("Adam", "Smith");
         var eventAssert = new PropertyChangedEventAssert(person);
         var errorsChangedAssert = new ErrorsChangedEventAssert(person);
         var changeAssert = new ChangeAssert(person);
         Assert.IsFalse(person.IsChanged, "Test setup failed, IsChanged is not false");
         Assert.AreEqual("Adam", person.FirstName, "Test setup failed, FirstName is not Adam");
         Assert.AreEqual("Smith", person.LastName, "Test setup failed, LastName is not Smith");
         person.FirstName = firstName;
         Assert.AreEqual(firstName, person.FirstName, "FirstName setter failed");
         Assert.AreEqual((firstName + " Smith").Trim(), person.FullName, "FullName not updated with FirstName changed");
         Assert.AreEqual(true, person.IsChanged, "IsChanged flag was not set when FirstName changed");
         eventAssert.Expect("IsChanged");
         eventAssert.Expect("FirstName");
         eventAssert.Expect("FullName");
         if (shouldHaveErrors)
         {
              Assert.IsTrue(person.HasErrors, "HasErrors should have remained false");
              errorsChangedAssert.ExpectCountEquals(1, "Expected an ErrorsChanged event");
              changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged", "HasErrors");
         }
         else 
         {
              errorsChangedAssert.ExpectNothing("Expected no ErrorsChanged events");
              changeAssert.AssertOnlyChangesAre("FirstName", "FullName", "IsChanged");
         }
    }

    テストコードが迷う前に、どこまで汎用化できるか、ここでは絶対に制限があります.しかし、意味のあるテスト名を付け、断言ごとに良い説明を付けることで、テストをより理解しやすくすることができます. 

    せいぎょへんすう


    現在,すべての断言はテスト用例の出力のみを考慮している.各Personオブジェクトの初期状態が既知であると仮定し、それから別の操作を行います.しかし、テストをより科学的にするには、変数を制御できることを確保しなければなりません.あるいは言い換えれば、私たちはすべてを把握していることを保証する必要があります.
    次のグループの断言を見てください.
    Assert.IsFalse(person.HasErrors, "Test setup failed, HasErrors is not false");
    Assert.IsFalse(person.IsChanged, "Test setup failed, IsChanged is not false");
    Assert.AreEqual("Adam", person.FirstName, "Test setup failed, FirstName is not Adam");
    Assert.AreEqual("Smith", person.LastName, "Test setup failed, LastName is not Smith");

    私たちはテストの開始ごとにこれらの断言を繰り返したくないので、私たちは彼らを工場の方法に移すことができて、このように私たちはいつもきれいなオブジェクトを手に入れることを保証することができます.これは、これらの設定を再利用して他の属性をテストするテスト例にも適用されます.
    [TestMethod]
    public void Person_FirstName_Set()
    {
         var person = GetAdamSmith();
         ... 

    表形式のテスト


    ここまで来たのは、「試験方法」の数が試験の完備度とは関係ないからだ.これらは、テストの使用例を組織し、実行するのに便利な方法にすぎません.
    もう1つの組織が大量のテスト例を組織する方法は、テーブル駆動テスト法である.単一のテストは実行できませんが、1行のコードだけで新しいテスト例を追加できます.テーブルフォーマットテストのテーブルはXMLのファイル、データベーステーブルに由来し、配列に書き込まれたり、同じ関数を使用して異なる値で繰り返し呼び出されたりすることができます.MBTestのようなフレームワークでは、属性でテスト例を与えることもできますが、例を軽くするためには、最小限の共通部分を維持しています.
    
    [TestMethod]
    public void Person_FullName_Tests()
    {     
         Person_FullName_Test("Bob", "Jones", "Bob Jones");
         Person_FullName_Test("Bob ", "Jones", "Bob Jones");
         Person_FullName_Test(" Bob", "Jones", "Bob Jones");
         Person_FullName_Test("Bob", " Jones", "Bob Jones");
         Person_FullName_Test("Bob", "Jones ", "Bob Jones");
         Person_FullName_Test(null, "Jones", "Jones");
         Person_FullName_Test(string.Empty, "Jones", "Jones");
         Person_FullName_Test("      ", "Jones", "Jones");
         Person_FullName_Test("Bob", "", "Bob");
         Person_FullName_Test("Bob", null, "Bob");
         Person_FullName_Test("Bob", string.Empty, "Bob");
         Person_FullName_Test("Bob", "      ", "Bob");
    }
    private void Person_FullName_Test(string firstName, string lastName, string expectedFullName)
    {
         var person = GetAdamSmith();
         person.FirstName = firstName;
         person.LastName = lastName;
         Assert.AreEqual(expectedFullName, person.FullName,
              string.Format("Incorrect full name when first name is '{0}' and last name is '{1}'"
              firstName ?? "
       
        "
       , lastName ?? "
       
        "
       ));
    }

    このテクニックを用いる場合は,パラメータ付きの誤った情報を用いることが重要である.追加しないと、どのパラメータの組み合わせが間違っているかを特定するときに、コードを一歩一歩デバッグする必要があります.

    結論


    任意の変数のセルテストを記述する場合は、次の要素を最大化してみたほうがいいです.
  • 有意義な単位ワークロードテストカバー率
  • 変動するコードベースラインに直面する場合、保守性
  • を保証する.
  • テストキットのパフォーマンス
  • 何をテストするのか、なぜ
  • なのかを明確に説明します.
    これらの要因が衝突することが多いことを考慮して、単一の用例の多重断言を慎重に運用することで、上記の4つの態様を向上させることができる.具体的な方法は、+作成する必要があるテンプレートコードの量を減らす+APIの変更によって更新する必要があるテンプレートコードの量を減らす+断言ごとに実行する必要があるテンプレートコードの数を減らす+ある操作のすべての断言をドキュメントで同じ場所に記録する

    作者について


    Jonathan Allenは2006年からInfoQのためにニュースを書いてきたが、現在は.NET分野の主任編集者.InfoQにニュースや教育記事を書くことに興味がある場合は、彼に連絡してください[email protected].
     
     
    原文住所:Writing a Comprehensive Unit Test