青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

C++ Programmer's Cookbook

{C++ 基礎(chǔ)} {C++ 高級(jí)} {C#界面,C++核心算法} {設(shè)計(jì)模式} {C#基礎(chǔ)}

使用 Visual Studio Team Test 進(jìn)行單元測(cè)試

演練:使用 Visual Studio Team Test 進(jìn)行單元測(cè)試

發(fā)布日期: 5/24/2005 | 更新日期: 5/24/2005

Mark Michaelis
Itron Corporation

摘要:本演練通過測(cè)試驅(qū)動(dòng)開發(fā) (TDD) 和先測(cè)試-后編碼 (test-then-code) 的方法學(xué)習(xí)單元測(cè)試。

*
本頁內(nèi)容
簡(jiǎn)介 簡(jiǎn)介
開始 開始
創(chuàng)建測(cè)試 創(chuàng)建測(cè)試
運(yùn)行測(cè)試 運(yùn)行測(cè)試
檢查異常 檢查異常
從數(shù)據(jù)庫中加載測(cè)試數(shù)據(jù) 從數(shù)據(jù)庫中加載測(cè)試數(shù)據(jù)
測(cè)試視圖 (Test View) 窗口 測(cè)試視圖 (Test View) 窗口
增加一個(gè)測(cè)試數(shù)據(jù)庫 增加一個(gè)測(cè)試數(shù)據(jù)庫
將數(shù)據(jù)與測(cè)試關(guān)聯(lián) 將數(shù)據(jù)與測(cè)試關(guān)聯(lián)
實(shí)現(xiàn)和重構(gòu)目標(biāo)方法 實(shí)現(xiàn)和重構(gòu)目標(biāo)方法
代碼覆蓋 代碼覆蓋
初始化和清除測(cè)試 初始化和清除測(cè)試
最佳實(shí)踐 最佳實(shí)踐
小結(jié) 小結(jié)

簡(jiǎn)介

最新發(fā)布的 Visual Studio Test System (VSTS) 包含了一套用于 Visual Studio Team Test 的完整功能。Team Test 是 Visual Studio 集成的單元測(cè)試框架,它支持:

?

測(cè)試方法存根 (stub) 的代碼生成。

?

在 IDE 中運(yùn)行測(cè)試。

?

合并從數(shù)據(jù)庫中加載的測(cè)試數(shù)據(jù)。

?

測(cè)試運(yùn)行完成后,進(jìn)行代碼覆蓋分析。

另外,Team Test 包含了一套測(cè)試功能,可以同時(shí)支持開發(fā)人員和測(cè)試人員。

在本文中,我們準(zhǔn)備演練如何創(chuàng)建Team Test 的單元測(cè)試。我們從一個(gè)簡(jiǎn)單的示例程序集開始,然后在該程序集中生成單元測(cè)試方法存根。這樣可以為Team Test 和單元測(cè)試的新手讀者提供基本的語法和代碼,同時(shí)也很好地介紹了如何快速建立測(cè)試項(xiàng)目的結(jié)構(gòu)。然后,我們轉(zhuǎn)到使用測(cè)試驅(qū)動(dòng)開發(fā) (test driven development, TDD) 方法,即在寫產(chǎn)品代碼前先寫單元測(cè)試。

Team Test的一個(gè)關(guān)鍵特點(diǎn)是從數(shù)據(jù)庫中加載測(cè)試數(shù)據(jù),然后將其用于測(cè)試方法。在演示基本的單元測(cè)試后,我們描述如何創(chuàng)建測(cè)試數(shù)據(jù)并集成到測(cè)試中。

本文中使用的示例項(xiàng)目包含一個(gè) LongonInfo 類,它封裝了與登錄相關(guān)的數(shù)據(jù)(例如用戶名和密碼)以及一些關(guān)于數(shù)據(jù)的簡(jiǎn)單的驗(yàn)證規(guī)則。最終的類如下圖 1 所示。


1. 最終的 LogonInfo

請(qǐng)注意所有的測(cè)試代碼位于一個(gè)單獨(dú)的項(xiàng)目。這是有道理的,產(chǎn)品代碼應(yīng)該盡可能少的受測(cè)試代碼影響,所以我們不想在產(chǎn)品代碼的程序集中嵌入測(cè)試代碼。

開始

首先,我們創(chuàng)建一個(gè)名為“VSTSDemo”的類庫項(xiàng)目。默認(rèn)情況下,為方案創(chuàng)建目錄(Create directory for solution) 復(fù)選框被選中。保留此選項(xiàng)可以使我們?cè)?VSTSDemo 項(xiàng)目的同一層目錄創(chuàng)建測(cè)試項(xiàng)目。相反,如果不選中此選項(xiàng),Visual Studio 2005 會(huì)將測(cè)試項(xiàng)目放在 VSTSDemo 項(xiàng)目的子目錄中。測(cè)試項(xiàng)目遵循 Visual Studio 在解決方案文件路徑的子目錄中創(chuàng)建額外項(xiàng)目的規(guī)定。

創(chuàng)建初始的 VSTSDemo 項(xiàng)目后,我們使用 Visual Studio 的解決方案資源管理器將 Class1.cs 文件重命名為 LogonInfo.cs,這樣類名也會(huì)被更新為 LogonInfo。然后我們修改構(gòu)造函數(shù)以接受兩個(gè)字符串參數(shù):userIdpassword。一旦構(gòu)造函數(shù)的簽名被聲明,我們就可以為構(gòu)造函數(shù)生成測(cè)試。


2. LongonInfo 構(gòu)造函數(shù)的上下文菜單的“創(chuàng)建測(cè)試 ( Create Tests... ) 菜單項(xiàng)

創(chuàng)建測(cè)試

在開始編寫 LogonInfo的任何實(shí)現(xiàn)之前,我們遵循 TDD 實(shí)踐的規(guī)則,首先編寫測(cè)試。TDD 在Team Test 中并不是必需的,但最好在本文的剩余部分遵循 TDD。右鍵單擊 LogonInfo()構(gòu)造函數(shù),然后選擇“創(chuàng)建測(cè)試…”菜單項(xiàng)(如圖 2 所示)。這樣會(huì)出現(xiàn)一個(gè)對(duì)話框,可以在不同的項(xiàng)目中生成單元測(cè)試(如圖 3 所示)。默認(rèn)情況下,項(xiàng)目設(shè)置的輸出 (Output) 選項(xiàng)是一個(gè)新的 Visual Basic 項(xiàng)目,但是也可以選擇 C# 和 C++ 測(cè)試項(xiàng)目。在本文中,我們選擇 Visual C#,然后單擊 OK 按鈕,接著輸入項(xiàng)目名 VSTSDemo.Test。測(cè)試項(xiàng)目名稱。


3. 生成單元測(cè)試對(duì)話框

生成的測(cè)試項(xiàng)目包含四個(gè)與測(cè)試相關(guān)的文件。

文件名 目的

AuthoringTest.txt

提供關(guān)于創(chuàng)建測(cè)試的說明,包括向項(xiàng)目增加其他測(cè)試的說明。

LogonInfoTest.cs

包含了用于測(cè)試 LogonInfo()的生成測(cè)試,以及測(cè)試初始化和測(cè)試清除的方法。

ManualTest1.mht

提供了一個(gè)模板,可以填入手工測(cè)試的指令。

UnitTest1.cs

一個(gè)空的單元測(cè)試類架構(gòu),用于放入另外的單元測(cè)試。

因?yàn)槲覀儾淮蛩銓?duì)該項(xiàng)目進(jìn)行手工測(cè)試,并且由于已經(jīng)有了一個(gè)單元測(cè)試文件,我們將刪除 ManualTest1.mht 和 UnitTest1.cs。

除了一些默認(rèn)的文件,生成的測(cè)試項(xiàng)目還包含了對(duì) Microsoft.VisualStudio.QualityTools.UnitTestFramework和 VSTSDemo 項(xiàng)目的引用。前者是測(cè)試引擎運(yùn)行單元測(cè)試需要依賴的測(cè)試框架程序集,后者是對(duì)我們需要測(cè)試的目標(biāo)程序集的項(xiàng)目引用。

默認(rèn)情況下,生成的測(cè)試方法是包含以下實(shí)現(xiàn)的占位符:

清單 1. 生成的測(cè)試方法: ConstructorTest(), 位于 VSTSDemo.Test.LogonInfoTest

   
/// <summary>
   ///This is a test class for VSTTDemo.LogonInfo and is intended
   ///to contain all VSTTDemo.LogonInfo Unit Tests
   ///</summary>
   [TestClass()]
   public class LogonInfoTest
   {
      // ...

      /// <summary>
      ///A test case for LogonInfo (string, string)
      ///</summary>
      [TestMethod()]
      public void ConstructorTest()
      {
         string userId = null; // TODO: Initialize to an appropriate value

         string password = null; // TODO: Initialize to an appropriate value

         LogonInfo target = new LogonInfo(userId, password);

         // TODO: Implement code to verify target
         Assert.Inconclusive(
            "TODO: Implement code to verify target");
      }
}

確切的生成代碼會(huì)根據(jù)測(cè)試目標(biāo)的方法類型和簽名不同而有所不同。例如,向?qū)?huì)為私有成員函數(shù)的測(cè)試生成反射代碼。在這種特別的情況下,我們需要專門用于公有構(gòu)造函數(shù)測(cè)試的代碼。

關(guān)于Team Test ,有兩個(gè)重要的特性。首先,作為測(cè)試的方法由 TestMethodAttribute屬性指定,另外,包含測(cè)試方法的類有 TestClassAttribute屬性。這些屬性都可以在 Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名空間中找到。Team Test 使用反射機(jī)制在測(cè)試程序集中搜索所有由 TestClass修飾的類,然后查找由 TestMethodAttribute修飾的方法來決定執(zhí)行的內(nèi)容。另外一個(gè)重要的由執(zhí)行引擎而不是編譯器驗(yàn)證的標(biāo)準(zhǔn)是,測(cè)試方法的簽名必須是無參數(shù)的實(shí)例方法。因?yàn)榉瓷渌阉?TestMethodAttribute,所以測(cè)試方法可以使用任意的名字。

測(cè)試方法 ConstructorTest()首先實(shí)例化目標(biāo) LongonInfo 類,然后斷言測(cè)試是非決定性的(使用Assert.Inconclusive())。當(dāng)測(cè)試運(yùn)行時(shí),Assert.Inconclusive()說明了它可能缺少正確的實(shí)現(xiàn)。在我們的示例中,我們更新 ConstructorTest()方法,讓它檢查用戶名和密碼的初始化,如下所示。

清單 2. 更新的 ConstructorTest() 實(shí)現(xiàn)

      
/// <summary>
      ///A test case for LogonInfo (string, string)
      ///</summary>
      [TestMethod()]
      public void ConstructorTest()
      {
         string userId = "IMontoya";

         string password = "P@ssw0rd";

         LogonInfo logonInfo = new LogonInfo(userId, password);

         Assert.AreEqual<string>(userId, logonInfo.UserId,
            "The UserId was not correctly initialized.");
         Assert.AreEqual<string>(password, logonInfo.Password,
            "The Password was not correctly initialized.");
      }

請(qǐng)注意我們的檢查使用 Assert.AreEqual<T>() 方法完成。Assert方法也支持沒有泛型的 AreEqual(),但是泛型版本幾乎總是首選,因?yàn)樗鼤?huì)在編譯時(shí)驗(yàn)證類型匹配 - 在 CLR 支持泛型前,這種錯(cuò)誤在單元測(cè)試框架中非常普遍。

因?yàn)?UserID 和 Password 的實(shí)例域還沒有創(chuàng)建,我們需要回頭將其添加到 LogonInfo類中,以便VSTTDemo.Test 項(xiàng)目可以編譯。

即使我們還沒有一個(gè)有效的實(shí)現(xiàn),讓我們開始運(yùn)行測(cè)試。如果我們遵循 TDD 方法,我們就應(yīng)該直到測(cè)試證明我們需要這樣的代碼時(shí)才去編寫產(chǎn)品代碼。我們僅在建立項(xiàng)目結(jié)構(gòu)時(shí)違背此原則,但是一旦項(xiàng)目建立后,就可以容易地始終遵循 TDD 方法。

運(yùn)行測(cè)試

要運(yùn)行項(xiàng)目中的所有測(cè)試,只需要運(yùn)行測(cè)試項(xiàng)目。要實(shí)現(xiàn)這一點(diǎn),我們需要右鍵單擊解決方案資源管理器的VSTSDemo.Test 項(xiàng)目,選擇設(shè)置為啟動(dòng)項(xiàng)目(Set as StartUp Project)。接著,使用菜單項(xiàng)調(diào)試->啟動(dòng)(F5) 或者調(diào)試->開始執(zhí)行(不調(diào)試)(Ctrl+F5) 開始運(yùn)行測(cè)試。

這時(shí)出現(xiàn)測(cè)試結(jié)果窗口,列出項(xiàng)目中的所有測(cè)試。因?yàn)槲覀兊捻?xiàng)目只包含一個(gè)測(cè)試,因此只列出了一個(gè)測(cè)試。開始的時(shí)候,測(cè)試會(huì)處于掛起的狀態(tài),但是一旦測(cè)試完成,結(jié)果將是我們意料中的失?。ㄈ鐖D 4 所示)。


4. 執(zhí)行所有測(cè)試后的測(cè)試結(jié)果窗口

圖 4 顯示了測(cè)試結(jié)果 (Test Results) 窗口。這個(gè)特別的屏幕快照除了默認(rèn)的列外,還顯示了錯(cuò)誤信息。您可以在列頭上單擊右鍵并選擇菜單項(xiàng)增加/刪除列…以增加或者刪除列。

如果要查看測(cè)試的額外細(xì)節(jié),我們可以選定測(cè)試并雙擊,打開“ConstructorTest[Results]”窗口,如圖 5 所示。


5. 詳細(xì)的測(cè)試 ConstructorTest [ Results ] 窗口

另外,我們可以右鍵單擊單個(gè)測(cè)試,然后選擇打開測(cè)試(Open Test) 菜單項(xiàng),進(jìn)入測(cè)試代碼。因?yàn)槲覀円呀?jīng)知道問題在于 LogonInfo 構(gòu)造函數(shù)的實(shí)現(xiàn),我們可以去那里編寫初始化 UserID Password 字段的代碼,使用傳入的參數(shù)對(duì)它們進(jìn)行初始化。重新運(yùn)行測(cè)試以驗(yàn)證測(cè)試現(xiàn)在可以通過。

檢查異常

下一步是創(chuàng)建 LongonInfo 類,以提供對(duì) UserID 和 password 的一些驗(yàn)證。不幸的是,UserIDPassword 字段是公共的,這意味著它們沒有提供任何封裝來確保它們有效。但是在我們將其轉(zhuǎn)換為屬性并提供驗(yàn)證前,讓我們編寫一些測(cè)試來驗(yàn)證任何實(shí)現(xiàn)的結(jié)果都是正確的。

我們首先來編寫一個(gè)測(cè)試,防止空值 (null) 或空字符串賦值給 UserID。預(yù)期結(jié)果是,如果空值傳送給構(gòu)造函數(shù),會(huì)引發(fā)一個(gè) ArgumentException異常。測(cè)試代碼如清單 3 所示。

清單 3. 使用 ExpectedExceptionAttribute 對(duì)異常情況進(jìn)行測(cè)試

      
[TestMethod]
      [ExpectedException(typeof(ArgumentException),
         "A userId of null was inappropriately allowed.")]
      public void NullUserIdInConstructor()
      {
         LogonInfo logonInfo = new LogonInfo(null, "P@ss0word");
      }

      [TestMethod]
      [ExpectedException(typeof(ArgumentException),
         "A empty userId was inappropriately allowed.")]
      public void EmptyUserIdInConstructor()
      {
         LogonInfo logonInfo = new LogonInfo("", "P@ss0word");
      }

請(qǐng)注意對(duì)于 ArgumentException沒有 try-catch 代碼塊的顯式測(cè)試。不過,兩個(gè)測(cè)試都包含另外一個(gè)屬性 ExpectedException,它接受一個(gè)類型參數(shù),以及一個(gè)可選的錯(cuò)誤信息,用于在沒有引發(fā)異常時(shí)顯示。當(dāng)這個(gè)單元測(cè)試執(zhí)行時(shí),測(cè)試框架會(huì)顯式地監(jiān)視引發(fā)的 ArgumentException異常,如果方法沒有引發(fā)這個(gè)異常,測(cè)試將失敗。運(yùn)行這些測(cè)試會(huì)證明我們還沒有對(duì) UserID 做任何驗(yàn)證檢查;因此,測(cè)試會(huì)失敗,因?yàn)闆]有引發(fā)預(yù)期的異常。

有了失敗的測(cè)試,現(xiàn)在可以回到產(chǎn)品代碼進(jìn)行更新來提供測(cè)試需要檢查的功能。在這個(gè)例子中,我們將 UserID字段轉(zhuǎn)換為屬性,并提供驗(yàn)證檢查(清單 4)。

清單 4. LogonInfo 類中驗(yàn)證 UserId

   
public class LogonInfo
   {
      public LogonInfo(string userId, string password)
      {
         this.UserId = userId;
         this.Password = password;
      }

      private string _UserId;
      public string UserId
      {
         get { return _UserId; }
         private set
         {
            if (value == null || value.Trim() == string.Empty)
            {
               throw new ArgumentException(
                  "Parameter userId may not be null or blank.");
            }
            _UserId = value;
         }
      }
      
      // ...
}

屬性的實(shí)現(xiàn)使用了 C# 2.0 的功能,其中 getter 和 setter 的訪問權(quán)限不一致。setter的實(shí)現(xiàn)標(biāo)識(shí)為私有,而 getter 實(shí)現(xiàn)為公有。這樣 UserID 就不能在 LogonInfo 類外被修改了(除非通過反射機(jī)制)。

一旦增加了驗(yàn)證,我們可以重新運(yùn)行測(cè)試來驗(yàn)證實(shí)現(xiàn)是正確的。我們運(yùn)行所有的三個(gè)測(cè)試來驗(yàn)證 UserID 字段轉(zhuǎn)換為屬性的重構(gòu)過程沒有產(chǎn)生任何意外的錯(cuò)誤。單元測(cè)試的真正價(jià)值在代碼修改的時(shí)候才真正有所體現(xiàn)。一套單元測(cè)試可以保證我們?cè)诰S護(hù)和改進(jìn)代碼的時(shí)候沒有破壞代碼。

從數(shù)據(jù)庫中加載測(cè)試數(shù)據(jù)

對(duì)于 LogonInfo 類的下一次修改,我們將提供一個(gè)方法來改變密碼。該方法接受舊密碼和新密碼作為參數(shù)。另外,我們會(huì)驗(yàn)證密碼符合某種復(fù)雜性需求。確切的說,我們將保證密碼符合 Windows Active Directory 的默認(rèn)需求,即包含以下四種類型字符中的三種:

?

大寫字母

?

小寫字母

?

標(biāo)點(diǎn)符號(hào)

?

數(shù)字

另外,我們將檢查密碼最少包含 6 個(gè)字符,最多包含 255 個(gè)字符。

和之前一樣,我們?cè)诰帉憣?shí)現(xiàn)前先為密碼復(fù)雜性需求編寫測(cè)試。但是顯然,我們需要提供一個(gè)測(cè)試值的大集合用于驗(yàn)證實(shí)現(xiàn)。我們不是為每個(gè)測(cè)試用例創(chuàng)建一個(gè)單獨(dú)的測(cè)試,也不是創(chuàng)建一個(gè)循環(huán)來調(diào)用一系列的測(cè)試用例,我們將創(chuàng)建一個(gè)數(shù)據(jù)驅(qū)動(dòng)測(cè)試,它從數(shù)據(jù)庫中取出所需的數(shù)據(jù)。

測(cè)試視圖 (Test View) 窗口

首先我們定義一個(gè)名為 ChangePasswordTest() 的新測(cè)試。定義后,從菜單項(xiàng)測(cè)試->查看和創(chuàng)建測(cè)試(Test->View and Author Tests)為測(cè)試方法打開測(cè)試視圖窗口,如圖 6 所示:


6. 測(cè)試視圖 ( Test view ) 窗口

測(cè)試視圖窗口可用來運(yùn)行指定的測(cè)試和瀏覽測(cè)試的特定屬性。通過增加額外的列(右鍵單擊列頭并選擇添加/刪除列…),我們可以排序并根據(jù)偏好查看測(cè)試。有些列來自修飾測(cè)試的屬性。例如,添加 OwnerAttribute將在所有者列顯示測(cè)試的所有者。其它元數(shù)據(jù)屬性(如 DescriptionAttribute也可以使用。這些屬性都可以在 Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名空間中找到。如果沒有顯式的屬性存在,那么我們可以使用自由形式的 TestPropertyAttribute來為特別的測(cè)試方法增加名-值對(duì)。

沒有對(duì)應(yīng)列的屬性可以在一個(gè)測(cè)試的屬性窗口中顯示(選擇一個(gè)測(cè)試,在右鍵上下文菜單中單擊屬性)。它包含了指定數(shù)據(jù)連接字符串和用于載入測(cè)試數(shù)據(jù)的表名的屬性。顯然,為了指定有效值,我們需要一個(gè)數(shù)據(jù)庫連接。

增加一個(gè)測(cè)試數(shù)據(jù)庫

從服務(wù)器資源管理器窗口,我們可以使用創(chuàng)建新的 SQL Server數(shù)據(jù)庫(Create new SQL Server Database) 菜單項(xiàng)。但是要小心這種方法,如果我們要在其它計(jì)算機(jī)上執(zhí)行測(cè)試的話,我們要保證在一臺(tái)服務(wù)器上創(chuàng)建數(shù)據(jù)庫,其它可能執(zhí)行測(cè)試的計(jì)算機(jī)必須能夠訪問該服務(wù)器 — 例如一臺(tái)用于構(gòu)建的計(jì)算機(jī)。

另外一個(gè)選擇是僅僅增加一個(gè)數(shù)據(jù)庫文件。使用項(xiàng)目->增加新項(xiàng) (Project->Add new item...) 允許向項(xiàng)目插入一個(gè) SQL 數(shù)據(jù)庫文件。這種方法使測(cè)試數(shù)據(jù)和測(cè)試項(xiàng)目保持在一起。缺點(diǎn)是如果數(shù)據(jù)庫變得很大,我們就不想這么做,而寧可提供全局的數(shù)據(jù)源。

對(duì)于本項(xiàng)目中的數(shù)據(jù),我們創(chuàng)建一個(gè)名為 VSTSDemo.mdf的本地項(xiàng)目數(shù)據(jù)庫文件。為了向文件加入測(cè)試數(shù)據(jù),我們使用菜單工具->連接到數(shù)據(jù)庫 (Tools->Connect to Database),然后指定 VSTSDemo.mdf 文件。然后,從服務(wù)器資源管理器窗口我們可以使用設(shè)計(jì)器加入一個(gè)新的表 LongonInfoTest。清單 5 顯示了該表的定義。

清單 5. LogonInfoTestData SQL 腳本

CREATE TABLE dbo.LogonInfoTest
   (
   UserId nchar(256) NOT NULL PRIMARY KEY CLUSTERED,
   Password nvarchar(256) NULL,
   IsValid bit NOT NULL
   )  ON [PRIMARY]
GO

保存表后,我們可以將其打開,然后輸入不同的非法密碼,如下表所示。

UserId Password IsValid

Humperdink

P@w0d

False

IMontoya

p@ssword

False

Inigo.Montoya

P@ssw0rd

False

Wesley

Password

False

將數(shù)據(jù)與測(cè)試關(guān)聯(lián)

一旦完成表的創(chuàng)建,我們需要將其與測(cè)試 InvalidPasswords()聯(lián)系起來。從測(cè)試 InvalidPasswords的屬性窗口,我們填寫數(shù)據(jù)連接字符串(Data Connection String) 數(shù)據(jù)表名 (Data Table Name) 屬性。這樣做將使用附加的屬性 DataSourceAttributeDataTableNameAttribute更新測(cè)試。最終的方法 ChangePasswordTest()在清單 6 中顯示。

清單 6. 用于數(shù)據(jù)驅(qū)動(dòng)測(cè)試的測(cè)試代碼

      
enum Column
      {
         UserId,
         Password,
         IsValid
      }

      private TestContext testContextInstance;

      /// <summary>
      ///Gets or sets the test context which provides
      ///information about and functionality for the 
      ///current test run.
      ///</summary>
      public TestContext TestContext
      {
         get
         {
            return testContextInstance;
         }
         set
         {
            testContextInstance = value;
         }
      }

      [TestMethod]
      [Owner("Mark Michaelis")]
      [TestProperty("TestCategory", "Developer"), 
      DataSource("System.Data.SqlClient", 
         "Data Source=.\\SQLEXPRESS;AttachDbFilename=\"<Path to the sample .mdf file>";Integrated 
Security=True", "LogonInfoTest", DataAccessMethod.Sequential)] public void ChangePasswordTest() { string userId = (string)TestContext.DataRow[(int)Column.UserId]; string password = (string)TestContext.DataRow[(int)Column.Password]; bool isValid = (bool)TestContext.DataRow[(int)Column.IsValid]; LogonInfo logonInfo = new LogonInfo(userId, "P@ssw0rd"); if (!isValid) { Exception exception = null; try { logonInfo.ChangePassword( "P@ssw0rd", password); } catch (Exception tempException) { exception = tempException; } Assert.IsNotNull(exception, "The expected exception was not thrown."); Assert.AreEqual<Type>( typeof(ArgumentException), exception.GetType(), "The exception type was unexpected."); } else { logonInfo.ChangePassword( "P@ssw0rd", password); Assert.AreEqual<string>(password, logonInfo.Password, "The password was not changed."); } }

清單 6 第一個(gè)需要注意的地方是增加了 DataSourceAttribute屬性,它指明了連接字符串、表名和訪問順序。在這個(gè)清單中,我們使用數(shù)據(jù)庫文件名標(biāo)識(shí)數(shù)據(jù)庫。這樣的優(yōu)點(diǎn)是該文件和測(cè)試項(xiàng)目一起遷移,假設(shè)它可能會(huì)被移動(dòng)到一個(gè)相對(duì)的路徑。

第二個(gè)注意的地方是 TestContext.DataRow調(diào)用。TestContext是在我們運(yùn)行創(chuàng)建測(cè)試向?qū)r(shí)由生成器提供的屬性,它在運(yùn)行時(shí)由測(cè)試執(zhí)行引擎自動(dòng)賦值,這樣我們就可以在測(cè)試中訪問跟測(cè)試環(huán)境關(guān)聯(lián)的數(shù)據(jù)。如圖 7 所示。


7. TestContext 關(guān)聯(lián)

如圖 7 所示,TestContext提供了 TestDirectoryTestName數(shù)據(jù),以及 BeginTimer()EndTimer()方法。對(duì) ChangePasswordTest()方法最有意義的是 DataRow屬性。因?yàn)?ChangePasswordTest()方法DataSourceAttribute修飾,該屬性指定的表返回每個(gè)記錄時(shí),該方法都會(huì)被調(diào)用一次。這就使測(cè)試代碼使用運(yùn)行中的測(cè)試的數(shù)據(jù),而且對(duì)插入 LongonInfoTest 表的每條記錄重復(fù)執(zhí)行測(cè)試。如果表包含四條記錄,那么測(cè)試將會(huì)分別執(zhí)行四次。

使用這樣的數(shù)據(jù)驅(qū)動(dòng)測(cè)試方法,可以很容易的提供額外的測(cè)試數(shù)據(jù),而不需要編寫任何代碼。一旦需要額外的測(cè)試用例,我們需要做的就是向 LongonInfoTest 表增加關(guān)聯(lián)的數(shù)據(jù)。盡管我們可以創(chuàng)建兩個(gè)獨(dú)立的測(cè)試來使用單獨(dú)的表分別測(cè)試有效和無效數(shù)據(jù),這個(gè)特定的例子合并了這些測(cè)試來顯示稍微復(fù)雜的數(shù)據(jù)測(cè)試實(shí)例。

實(shí)現(xiàn)和重構(gòu)目標(biāo)方法

現(xiàn)在我們已經(jīng)有了測(cè)試,是時(shí)候?yàn)闇y(cè)試編寫實(shí)現(xiàn)了。使用 C# 重構(gòu)工具,我們可以右鍵單擊 ChangePassword()方法調(diào)用,選擇菜單項(xiàng)GenerateMethodStub,然后對(duì)于生成的方法提供實(shí)現(xiàn),一旦我們成功地運(yùn)行了使用所有測(cè)試數(shù)據(jù)的測(cè)試,我們也可以開始重構(gòu)代碼了,LogonInfo 類的最終實(shí)現(xiàn)如清單 7 所示。

清單 7. LogonInfo

using System;
using System.Text.RegularExpressions;

namespace VSTTDemo
{
   public class LogonInfo
   {
      public LogonInfo(string userId, string password)
      {
         this.UserId = userId;
         this.Password = password;
      }

      private string _UserId;
      public string UserId
      {
         get { return _UserId; }
         private set
         {
            if (value == null || value.Trim() == string.Empty)
            {
               throw new ArgumentException(
                  "Parameter userId may not be null or blank.");
            }
            _UserId = value;
         }
      }

      private string _Password;
      public string Password
      {
         get { return _Password; }
         private set
         {
            string errorMessage;
            if (!IsValidPassword(value, out errorMessage))
            {
               throw new ArgumentException(
                  errorMessage);
            }
            _Password = value;
         }
      }

      public static bool IsValidPassword(string value, 
         out string errorMessage)
      {
         const string passwordSizeRegex = "(?=^.{6,255}$)";
         const string uppercaseRegex = "(?=.*[A-Z])";
         const string lowercaseRegex = "(?=.*[a-z])";
         const string punctuationRegex = @"(?=.*\d)";
         const string upperlowernumericRegex = "(?=.*[^A-Za-z0-9])";

         bool isValid;
         Regex regex = new Regex(
            passwordSizeRegex +
            "(" + punctuationRegex + uppercaseRegex + lowercaseRegex +
               "|" + punctuationRegex + upperlowernumericRegex + lowercaseRegex +
               "|" + upperlowernumericRegex + uppercaseRegex + lowercaseRegex +
               "|" + punctuationRegex + uppercaseRegex + upperlowernumericRegex +
            ")^.*");

         if (value == null || value.Trim() == string.Empty)
         {
            isValid = false;
            errorMessage = "Password may not be null or blank.";
         }
         else
         {
            if (regex.Match(value).Success)
            {
               isValid = true;
               errorMessage = "";
            }
            else
            {
               isValid = false;
               errorMessage = "Password does not meet the complexity requirements.";
            }
         }
         return isValid;
      }

      public void ChangePassword(
         string oldPassword, string newPassword)
      {
         if (oldPassword == Password)
         {
            Password = newPassword;
         }
         else
         {
            throw new ArgumentException(
               "The old password was not correct.");
         }
      }
   }
}

代碼覆蓋

單元測(cè)試的一個(gè)關(guān)鍵度量是決定在單元測(cè)試運(yùn)行時(shí)測(cè)試了多少代碼。該度量稱為代碼覆蓋,Team Test 包含了一個(gè)代碼覆蓋工具,可以詳細(xì)解釋被執(zhí)行代碼的百分率,并突出顯示哪些代碼被執(zhí)行,那些沒有被執(zhí)行。該功能如圖 8 所示。


8. 突出顯示代碼覆蓋

圖 8 顯示了運(yùn)行所有單元測(cè)試后的代碼覆蓋的突出顯示情況。紅色突出顯示說明了我們有產(chǎn)品代碼沒有運(yùn)行任何單元測(cè)試,這說明我們編寫這些代碼時(shí)未遵循 TDD 原則,即在編寫實(shí)現(xiàn)前先提供測(cè)試。

初始化和清除測(cè)試

一般來說,測(cè)試類不僅包含獨(dú)立的測(cè)試方法,還包含了不同的對(duì)測(cè)試進(jìn)行初始化和清除的方法。實(shí)際上,創(chuàng)建測(cè)試向?qū)г趧?chuàng)建 VSTSDemo.Test 項(xiàng)目時(shí),將一些這樣的方法添加到類 LongonInfoTest 中,見清單 8。

清單 8. 最終的 LogonInfoTest

using VSTTDemo;
using Microsoft.VisualStudio.QualityTools.UnitTesting.Framework;
using System;

namespace VSTSDemo.Test
{
   /// <summary>
   ///This is a test class for VSTTDemo.LogonInfo and is intended
   ///to contain all VSTTDemo.LogonInfo Unit Tests
   ///</summary>
   [TestClass()]
   public class LogonInfoTest
   {

      private TestContext testContextInstance;

      /// <summary>
      ///Gets or sets the test context which provides
      ///information about and functionality for the 
      ///current test run.
      ///</summary>
      public TestContext TestContext
      {
         get
         {
            return testContextInstance;
         }
         set
         {
            testContextInstance = value;
         }
      }

      /// <summary>
      ///Initialize() is called once during test execution before
      ///test methods in this test class are executed.
      ///</summary>
      [TestInitialize()]
      public void Initialize()
      {
         //  TODO: Add test initialization code
      }

      /// <summary>
      ///Cleanup() is called once during test execution after
      ///test methods in this class have executed unless
      ///this test class' Initialize() method throws an exception.
      ///</summary>
      [TestCleanup()]
      public void Cleanup()
      {
         
         //  TODO: Add test cleanup code
      }


      // ...

      [TestMethod]
      // ...
      public void ChangePasswordTest()
      {   
      // ...
      }

   }
}

用于對(duì)測(cè)試進(jìn)行設(shè)置和清除的方法分別由屬性 TestInitializeAttributeTestCleanupAttribute修飾。在每個(gè)這樣的方法中,我們可以加入額外的代碼,它們將會(huì)在每個(gè)測(cè)試前或者測(cè)試后運(yùn)行。這意味著在每次對(duì)應(yīng)于 LongonInfoTest 表的記錄的 ChangePasswordTest()執(zhí)行前,Initialize() Cleanup() 都會(huì)被執(zhí)行,每次 NullUserIdInConstructorEmptyUserIdInConstructor執(zhí)行時(shí)也會(huì)發(fā)生同樣的情況。這樣的方法可以用于向數(shù)據(jù)庫中插入默認(rèn)的數(shù)據(jù),然后在測(cè)試完成時(shí)清除插入的數(shù)據(jù)。例如,我們可以做到在 Initialize()中開始一個(gè)事務(wù),然后在清除時(shí)回滾同一個(gè)事務(wù),這樣一來,如果測(cè)試方法使用相同的連接時(shí),數(shù)據(jù)狀態(tài)會(huì)在每次測(cè)試執(zhí)行完成時(shí)恢復(fù)原狀。類似地,測(cè)試文件也可以這樣處理。

在調(diào)試期間,TestCleanupAttribute修飾的方法可能由于調(diào)試器在清除的代碼執(zhí)行前終止運(yùn)行。由于這個(gè)原因,最好在設(shè)置測(cè)試期間檢查清除情況,并在需要時(shí)在設(shè)置測(cè)試前執(zhí)行清除代碼。關(guān)于初始化和清除的其它可用的測(cè)試屬性有 AssemblyInitializeAttribute/AssemblyCleanupAttributeClassInitializeAttribute/ClassCleanupAttribute。程序集相關(guān)的屬性對(duì)整個(gè)程序集運(yùn)行一次,而類相關(guān)的屬性對(duì)一個(gè)特定的測(cè)試類的加載運(yùn)行一次。

最佳實(shí)踐

在結(jié)束前我們回顧幾種單元測(cè)試的最佳實(shí)踐。首先,TDD 是非常有價(jià)值的實(shí)踐。在所有現(xiàn)有的開發(fā)方法中,TDD 可能是多年來根本上改進(jìn)開發(fā)且投資成本最小的一種。每個(gè) QA 工程師都會(huì)告訴您,開發(fā)人員在沒有相應(yīng)的測(cè)試前不會(huì)寫出成功的軟件。有了 TDD,實(shí)踐是在實(shí)現(xiàn)前編寫測(cè)試,并且理想情況是,編寫的測(cè)試可以成為無需人工參與執(zhí)行的構(gòu)建腳本的一部分。需要訓(xùn)練來開始養(yǎng)成習(xí)慣,但一旦建立習(xí)慣后,不使用 TDD 方法編碼就像開車時(shí)不系安全帶一樣。

對(duì)于測(cè)試本身,有一些額外的原則可以幫助成功進(jìn)行測(cè)試:

?

避免測(cè)試產(chǎn)生依賴性,這樣測(cè)試需要按照特定的順序執(zhí)行。每個(gè)測(cè)試都應(yīng)該是自治的。

?

使用測(cè)試初始化代碼驗(yàn)證測(cè)試清除已經(jīng)成功執(zhí)行,如果沒有則在執(zhí)行測(cè)試前重新執(zhí)行清除。

?

在編寫任何產(chǎn)品代碼的實(shí)現(xiàn)前編寫測(cè)試。

?

對(duì)于產(chǎn)品代碼中的每個(gè)類創(chuàng)建一個(gè)測(cè)試類。這樣可以簡(jiǎn)化測(cè)試的組織,并可以容易地選擇在何處放置每個(gè)測(cè)試。

?

使用 Visual Studio 生成初始化的測(cè)試項(xiàng)目。這樣可以大大減少手工設(shè)置測(cè)試項(xiàng)目并與產(chǎn)品項(xiàng)目關(guān)聯(lián)的步驟。

?

避免創(chuàng)建其他依賴計(jì)算機(jī)的測(cè)試,例如依賴特定的目錄路徑的測(cè)試。

?

創(chuàng)建模擬對(duì)象 (mock object) 來測(cè)試接口。模擬對(duì)象通常在需要驗(yàn)證 API 符合所需功能的測(cè)試項(xiàng)目中實(shí)現(xiàn)。

?

在繼續(xù)創(chuàng)建新的測(cè)試前驗(yàn)證所有測(cè)試運(yùn)行成功。這樣可以保證在破壞代碼后立刻進(jìn)行修正。

?

可以最大化無需人工參與執(zhí)行的測(cè)試代碼。在依賴于手工測(cè)試前,必須完全肯定無法采用合理的無需人工參與執(zhí)行的測(cè)試方案。

小結(jié)

總的來說,VSTS 的單元測(cè)試功能本身很好理解。而且盡管本文沒有提到,它還可以通過自定義執(zhí)行引擎進(jìn)行擴(kuò)展。此外,它包含了代碼覆蓋分析的功能,這對(duì)于評(píng)價(jià)測(cè)試的全面性非常有用。通過使用 VSTS,您可以將測(cè)試數(shù)目和 bug 數(shù)目或編寫的代碼數(shù)量進(jìn)行關(guān)聯(lián)比較。這為項(xiàng)目的運(yùn)行狀況提供了很好的指標(biāo)。

本文介紹了Team Test 產(chǎn)品中的基本單元測(cè)試功能,也探討了關(guān)于數(shù)據(jù)驅(qū)動(dòng)測(cè)試的一些更加高級(jí)的功能。通過開始實(shí)踐對(duì)代碼進(jìn)行單元測(cè)試,您會(huì)為產(chǎn)品的整個(gè)生命期建立一套寶貴的測(cè)試集。Team Test 通過與 Visual Studio 的強(qiáng)大集成和其它 VSTS 產(chǎn)品線,使這一切變得容易。

Mark Michaelis 在 Itron 公司擔(dān)任軟件架構(gòu)師和講師。他曾經(jīng)對(duì)幾個(gè)微軟的產(chǎn)品設(shè)計(jì)進(jìn)行檢查,包括 C# 和VSTS?,F(xiàn)在他正在撰寫另外一本有關(guān) C# 的書,Essential C# (Addison Wesley)。不使用計(jì)算機(jī)時(shí),他會(huì)陪伴家人,進(jìn)行戶外運(yùn)動(dòng),或者進(jìn)行環(huán)球旅行。Mark Michaelis 住在 Spokane, WA。您可以通過 mark@michaelis.net 和他聯(lián)系或者訪問他的網(wǎng)絡(luò)日志:http://mark.michaelis.net。

轉(zhuǎn)到原英文頁面

翻譯者Luke是微軟公司的軟件工程師,習(xí)慣使用C++和C#開發(fā)應(yīng)用程序。閑暇時(shí)間他喜歡音樂,旅游和懷舊游戲,并且愿意幫助MSDN翻譯更多的文章和其他開發(fā)者共享。可以通過ecaijw@msn.com聯(lián)系他。

posted on 2006-06-16 11:46 夢(mèng)在天涯 閱讀(1463) 評(píng)論(1)  編輯 收藏 引用 所屬分類: C#/.NET 、VS2005/2008

評(píng)論

# re: 使用 Visual Studio Team Test 進(jìn)行單元測(cè)試 2006-06-16 11:47 夢(mèng)在天涯

原文:http://www.microsoft.com/china/msdn/library/langtool/vsts/sentvstsover.mspx?mfr=true  回復(fù)  更多評(píng)論   

公告

EMail:itech001#126.com

導(dǎo)航

統(tǒng)計(jì)

  • 隨筆 - 461
  • 文章 - 4
  • 評(píng)論 - 746
  • 引用 - 0

常用鏈接

隨筆分類

隨筆檔案

收藏夾

Blogs

c#(csharp)

C++(cpp)

Enlish

Forums(bbs)

My self

Often go

Useful Webs

Xml/Uml/html

搜索

  •  

積分與排名

  • 積分 - 1812199
  • 排名 - 5

最新評(píng)論

閱讀排行榜

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
      <noscript id="pjuwb"></noscript>
            <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
              <dd id="pjuwb"></dd>
              <abbr id="pjuwb"></abbr>
              久久se精品一区二区| 99在线精品视频在线观看| 久久久国产精品一区二区中文| 国产精品网红福利| 久久久综合精品| 女主播福利一区| 亚洲一区网站| 久久精品成人| 99精品免费视频| 亚洲综合视频一区| 亚洲福利视频网站| 日韩视频在线观看| 国产日韩视频| 亚洲国产精品久久久| 欧美精品在线一区二区| 亚洲欧美日韩综合| 亚洲午夜国产一区99re久久| 久久综合色88| 欧美日韩国产123| 欧美一区二视频在线免费观看| 久久精品国内一区二区三区| aa国产精品| 久久九九热免费视频| 99国产精品一区| 欧美亚洲一区二区三区| 亚洲欧洲综合另类在线| 亚洲专区一区| 亚洲精品美女| 久久精品国产免费| 香蕉尹人综合在线观看| 欧美国产日韩一二三区| 久久国产精品99国产精| 欧美另类女人| 欧美顶级艳妇交换群宴| 国产欧美日韩亚洲一区二区三区| 欧美风情在线| 国产视频精品va久久久久久| 亚洲九九精品| 亚洲国产你懂的| 久久精品一本| 欧美一区二区三区在线播放| 欧美精品123区| 欧美aⅴ99久久黑人专区| 国产欧美精品一区| 亚洲小视频在线| 亚洲视频综合| 欧美精品成人在线| 欧美激情亚洲一区| 欲香欲色天天天综合和网| 亚洲欧美国产77777| 亚洲主播在线观看| 欧美日韩一级片在线观看| 亚洲国产精选| 亚洲精品国产无天堂网2021| 久久综合久久综合久久| 蜜桃精品一区二区三区 | 亚洲视频一区在线观看| 亚洲伦理在线免费看| 老司机一区二区| 久久人人超碰| 黑人巨大精品欧美一区二区小视频 | 亚洲人成在线观看| 久久久精彩视频| 久久综合亚州| 在线观看国产成人av片| 蜜桃av一区二区三区| 久热精品视频在线| 亚洲第一页在线| 免费不卡亚洲欧美| 亚洲激情视频在线观看| 日韩一二三在线视频播| 欧美日韩在线播放三区| 亚洲一二三四久久| 久久精品成人一区二区三区| 国产主播一区二区三区四区| 久久久久久高潮国产精品视| 欧美a级一区| 一本大道久久a久久精二百| 亚洲欧洲免费视频| 国产日韩一区欧美| 久久成人免费| 亚洲高清激情| 亚洲一区日韩| 国产亚洲精品一区二区| 久久精品日产第一区二区| 亚洲第一网站| 亚洲欧美日韩精品久久亚洲区 | 亚洲电影自拍| 欧美日韩亚洲一区二区三区在线| 亚洲免费一在线| 欧美成人一区二区三区| 亚洲网站在线播放| 国产日韩欧美二区| 欧美激情综合| 久久国产精品99精品国产| 欧美激情中文不卡| 亚洲欧美日韩国产一区二区| 在线欧美日韩| 国产精品r级在线| 久久大综合网| 99精品欧美一区二区三区| 久久人人爽人人爽爽久久| 亚洲毛片一区| 国产一区白浆| 欧美性感一类影片在线播放| 久久亚洲风情| 在线亚洲一区观看| 欧美搞黄网站| 久久精品二区亚洲w码| 在线亚洲精品| 亚洲经典一区| 国内精品久久久久久影视8 | 国产精品私拍pans大尺度在线| 久久夜色精品国产亚洲aⅴ| 一区二区三区欧美在线| 欧美激情成人在线视频| 久久久97精品| 午夜精品一区二区三区电影天堂| 亚洲精品视频在线播放| 国内精品久久久久伊人av| 国产精品vvv| 欧美日韩国产高清视频| 蜜臀a∨国产成人精品 | 亚洲精品自在久久| 免费在线观看一区二区| 久久精品国产欧美亚洲人人爽| 99精品热视频| 亚洲乱码国产乱码精品精可以看| 激情综合激情| 黄色亚洲在线| 国语自产在线不卡| 国产区二精品视| 国产精品亚洲不卡a| 欧美性猛片xxxx免费看久爱 | 久久精品一区二区国产| 欧美专区第一页| 久久国产欧美日韩精品| 欧美一区二区在线| 欧美一区高清| 久久久国产91| 久久一日本道色综合久久| 久久亚洲精品中文字幕冲田杏梨| 久久精品国产综合| 久久久在线视频| 美女91精品| 欧美精品日日鲁夜夜添| 老司机久久99久久精品播放免费| 免费在线看成人av| 欧美福利视频一区| 亚洲电影视频在线| 91久久国产自产拍夜夜嗨| 亚洲人成小说网站色在线| 一本色道久久88综合日韩精品| 99精品热6080yy久久| 亚洲制服av| 久久久久国产免费免费| 欧美jjzz| 国产精品电影网站| 国产在线播放一区二区三区| 亚洲高清视频的网址| 日韩亚洲欧美成人| 亚洲欧美日韩中文在线制服| 久久精品成人一区二区三区蜜臀 | 香蕉久久夜色| 久久精品国产亚洲一区二区三区| 久久久久久亚洲综合影院红桃| 免费欧美日韩| 一区二区三区成人精品| 欧美中文字幕在线| 欧美激情久久久| 国产精品一区二区三区乱码| 尤物九九久久国产精品的分类| 亚洲精品综合精品自拍| 午夜欧美大片免费观看| 蜜臀久久99精品久久久久久9| 亚洲国产色一区| 欧美亚洲综合另类| 欧美黑人一区二区三区| 国产欧美日韩91| 一区二区高清在线| 快射av在线播放一区| 一本久久精品一区二区| 久久久久久久激情视频| 国产精品久久久久久久久果冻传媒| 狠狠色狠狠色综合日日tαg | 依依成人综合视频| 亚洲色图综合久久| 欧美va天堂va视频va在线| 中文精品视频| 免费美女久久99| 国产午夜精品一区二区三区视频 | 欧美成人精品| 性色av一区二区三区红粉影视| 欧美a级在线| 一区精品久久| 欧美在线网址| 亚洲一区二区三区成人在线视频精品| 久久中文在线| 黄色日韩网站视频| 久久riav二区三区|