演練:使用 Visual Studio Team Test 進(jìn)行單元測(cè)試
發(fā)布日期: 5/24/2005 | 更新日期: 5/24/2005
Mark Michaelis
Itron Corporation
摘要:本演練通過(guò)測(cè)試驅(qū)動(dòng)開(kāi)發(fā) (TDD) 和先測(cè)試-后編碼 (test-then-code) 的方法學(xué)習(xí)單元測(cè)試。
本頁(yè)內(nèi)容
簡(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ù)庫(kù)中加載的測(cè)試數(shù)據(jù)。
|
? |
測(cè)試運(yùn)行完成后,進(jìn)行代碼覆蓋分析。
|
另外,Team Test 包含了一套測(cè)試功能,可以同時(shí)支持開(kāi)發(fā)人員和測(cè)試人員。
在本文中,我們準(zhǔn)備演練如何創(chuàng)建Team Test 的單元測(cè)試。我們從一個(gè)簡(jiǎn)單的示例程序集開(kāi)始,然后在該程序集中生成單元測(cè)試方法存根。這樣可以為T(mén)eam Test 和單元測(cè)試的新手讀者提供基本的語(yǔ)法和代碼,同時(shí)也很好地介紹了如何快速建立測(cè)試項(xiàng)目的結(jié)構(gòu)。然后,我們轉(zhuǎn)到使用測(cè)試驅(qū)動(dòng)開(kāi)發(fā) (test driven development, TDD) 方法,即在寫(xiě)產(chǎn)品代碼前先寫(xiě)單元測(cè)試。
Team Test的一個(gè)關(guān)鍵特點(diǎn)是從數(shù)據(jù)庫(kù)中加載測(cè)試數(shù)據(jù),然后將其用于測(cè)試方法。在演示基本的單元測(cè)試后,我們描述如何創(chuàng)建測(cè)試數(shù)據(jù)并集成到測(cè)試中。
本文中使用的示例項(xiàng)目包含一個(gè) LongonInfo 類(lèi),它封裝了與登錄相關(guān)的數(shù)據(jù)(例如用戶名和密碼)以及一些關(guān)于數(shù)據(jù)的簡(jiǎn)單的驗(yàn)證規(guī)則。最終的類(lèi)如下圖 1 所示。
圖
1.
最終的
LogonInfo
類(lèi)
請(qǐng)注意所有的測(cè)試代碼位于一個(gè)單獨(dú)的項(xiàng)目。這是有道理的,產(chǎn)品代碼應(yīng)該盡可能少的受測(cè)試代碼影響,所以我們不想在產(chǎn)品代碼的程序集中嵌入測(cè)試代碼。
開(kāi)始
首先,我們創(chuàng)建一個(gè)名為“VSTSDemo”的類(lèi)庫(kù)項(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,這樣類(lèi)名也會(huì)被更新為 LogonInfo。然后我們修改構(gòu)造函數(shù)以接受兩個(gè)字符串參數(shù):userId 和 password。一旦構(gòu)造函數(shù)的簽名被聲明,我們就可以為構(gòu)造函數(shù)生成測(cè)試。
圖
2.
LongonInfo
構(gòu)造函數(shù)的上下文菜單的“創(chuàng)建測(cè)試
…
”
(
Create Tests...
)
菜單項(xiàng)
創(chuàng)建測(cè)試
在開(kāi)始編寫(xiě) LogonInfo的任何實(shí)現(xiàn)之前,我們遵循 TDD 實(shí)踐的規(guī)則,首先編寫(xiě)測(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è)試的說(shuō)明,包括向項(xiàng)目增加其他測(cè)試的說(shuō)明。
|
LogonInfoTest.cs
|
包含了用于測(cè)試 LogonInfo()的生成測(cè)試,以及測(cè)試初始化和測(cè)試清除的方法。
|
ManualTest1.mht
|
提供了一個(gè)模板,可以填入手工測(cè)試的指令。
|
UnitTest1.cs
|
一個(gè)空的單元測(cè)試類(lèi)架構(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)的方法類(lèi)型和簽名不同而有所不同。例如,向?qū)?huì)為私有成員函數(shù)的測(cè)試生成反射代碼。在這種特別的情況下,我們需要專(zhuān)門(mén)用于公有構(gòu)造函數(shù)測(cè)試的代碼。
關(guān)于Team Test ,有兩個(gè)重要的特性。首先,作為測(cè)試的方法由 TestMethodAttribute屬性指定,另外,包含測(cè)試方法的類(lèi)有 TestClassAttribute屬性。這些屬性都可以在 Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名空間中找到。Team Test 使用反射機(jī)制在測(cè)試程序集中搜索所有由 TestClass修飾的類(lèi),然后查找由 TestMethodAttribute修飾的方法來(lái)決定執(zhí)行的內(nèi)容。另外一個(gè)重要的由執(zhí)行引擎而不是編譯器驗(yàn)證的標(biāo)準(zhǔn)是,測(cè)試方法的簽名必須是無(wú)參數(shù)的實(shí)例方法。因?yàn)榉瓷渌阉?TestMethodAttribute,所以測(cè)試方法可以使用任意的名字。
測(cè)試方法 ConstructorTest()首先實(shí)例化目標(biāo) LongonInfo 類(lèi),然后斷言測(cè)試是非決定性的(使用Assert.Inconclusive())。當(dāng)測(cè)試運(yùn)行時(shí),Assert.Inconclusive()說(shuō)明了它可能缺少正確的實(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方法也支持沒(méi)有泛型的 AreEqual(),但是泛型版本幾乎總是首選,因?yàn)樗鼤?huì)在編譯時(shí)驗(yàn)證類(lèi)型匹配 - 在 CLR 支持泛型前,這種錯(cuò)誤在單元測(cè)試框架中非常普遍。
因?yàn)?UserID 和 Password 的實(shí)例域還沒(méi)有創(chuàng)建,我們需要回頭將其添加到 LogonInfo類(lèi)中,以便VSTTDemo.Test 項(xiàng)目可以編譯。
即使我們還沒(méi)有一個(gè)有效的實(shí)現(xiàn),讓我們開(kāi)始運(yùn)行測(cè)試。如果我們遵循 TDD 方法,我們就應(yīng)該直到測(cè)試證明我們需要這樣的代碼時(shí)才去編寫(xiě)產(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)試->開(kāi)始執(zhí)行(不調(diào)試)(Ctrl+F5) 開(kāi)始運(yùn)行測(cè)試。
這時(shí)出現(xiàn)測(cè)試結(jié)果窗口,列出項(xiàng)目中的所有測(cè)試。因?yàn)槲覀兊捻?xiàng)目只包含一個(gè)測(cè)試,因此只列出了一個(gè)測(cè)試。開(kāi)始的時(shí)候,測(cè)試會(huì)處于掛起的狀態(tài),但是一旦測(cè)試完成,結(jié)果將是我們意料中的失敗(如圖 4 所示)。
圖
4.
執(zhí)行所有測(cè)試后的測(cè)試結(jié)果窗口
圖 4 顯示了測(cè)試結(jié)果 (Test Results) 窗口。這個(gè)特別的屏幕快照除了默認(rèn)的列外,還顯示了錯(cuò)誤信息。您可以在列頭上單擊右鍵并選擇菜單項(xiàng)增加/刪除列…以增加或者刪除列。
如果要查看測(cè)試的額外細(xì)節(jié),我們可以選定測(cè)試并雙擊,打開(kāi)“ConstructorTest[Results]”窗口,如圖 5 所示。
圖
5.
詳細(xì)的測(cè)試
ConstructorTest [
Results
]
窗口
另外,我們可以右鍵單擊單個(gè)測(cè)試,然后選擇打開(kāi)測(cè)試(Open Test) 菜單項(xiàng),進(jìn)入測(cè)試代碼。因?yàn)槲覀円呀?jīng)知道問(wèn)題在于 LogonInfo 構(gòu)造函數(shù)的實(shí)現(xiàn),我們可以去那里編寫(xiě)初始化 UserID 和 Password 字段的代碼,使用傳入的參數(shù)對(duì)它們進(jìn)行初始化。重新運(yùn)行測(cè)試以驗(yàn)證測(cè)試現(xiàn)在可以通過(guò)。
檢查異常
下一步是創(chuàng)建 LongonInfo 類(lèi),以提供對(duì) UserID 和 password 的一些驗(yàn)證。不幸的是,UserID和 Password 字段是公共的,這意味著它們沒(méi)有提供任何封裝來(lái)確保它們有效。但是在我們將其轉(zhuǎn)換為屬性并提供驗(yàn)證前,讓我們編寫(xiě)一些測(cè)試來(lái)驗(yàn)證任何實(shí)現(xiàn)的結(jié)果都是正確的。
我們首先來(lái)編寫(xiě)一個(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沒(méi)有 try-catch 代碼塊的顯式測(cè)試。不過(guò),兩個(gè)測(cè)試都包含另外一個(gè)屬性 ExpectedException,它接受一個(gè)類(lèi)型參數(shù),以及一個(gè)可選的錯(cuò)誤信息,用于在沒(méi)有引發(fā)異常時(shí)顯示。當(dāng)這個(gè)單元測(cè)試執(zhí)行時(shí),測(cè)試框架會(huì)顯式地監(jiān)視引發(fā)的 ArgumentException異常,如果方法沒(méi)有引發(fā)這個(gè)異常,測(cè)試將失敗。運(yùn)行這些測(cè)試會(huì)證明我們還沒(méi)有對(duì) UserID 做任何驗(yàn)證檢查;因此,測(cè)試會(huì)失敗,因?yàn)闆](méi)有引發(fā)預(yù)期的異常。
有了失敗的測(cè)試,現(xiàn)在可以回到產(chǎn)品代碼進(jìn)行更新來(lái)提供測(cè)試需要檢查的功能。在這個(gè)例子中,我們將 UserID字段轉(zhuǎn)換為屬性,并提供驗(yàn)證檢查(清單 4)。
清單
4.
在
LogonInfo
類(lèi)中驗(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 的訪問(wèn)權(quán)限不一致。setter的實(shí)現(xiàn)標(biāo)識(shí)為私有,而 getter 實(shí)現(xiàn)為公有。這樣 UserID 就不能在 LogonInfo 類(lèi)外被修改了(除非通過(guò)反射機(jī)制)。
一旦增加了驗(yàn)證,我們可以重新運(yùn)行測(cè)試來(lái)驗(yàn)證實(shí)現(xiàn)是正確的。我們運(yùn)行所有的三個(gè)測(cè)試來(lái)驗(yàn)證 UserID 字段轉(zhuǎn)換為屬性的重構(gòu)過(guò)程沒(méi)有產(chǎn)生任何意外的錯(cuò)誤。單元測(cè)試的真正價(jià)值在代碼修改的時(shí)候才真正有所體現(xiàn)。一套單元測(cè)試可以保證我們?cè)诰S護(hù)和改進(jìn)代碼的時(shí)候沒(méi)有破壞代碼。
從數(shù)據(jù)庫(kù)中加載測(cè)試數(shù)據(jù)
對(duì)于 LogonInfo 類(lèi)的下一次修改,我們將提供一個(gè)方法來(lái)改變密碼。該方法接受舊密碼和新密碼作為參數(shù)。另外,我們會(huì)驗(yàn)證密碼符合某種復(fù)雜性需求。確切的說(shuō),我們將保證密碼符合 Windows Active Directory 的默認(rèn)需求,即包含以下四種類(lèi)型字符中的三種:
? |
大寫(xiě)字母
|
? |
小寫(xiě)字母
|
? |
標(biāo)點(diǎn)符號(hào)
|
? |
數(shù)字
|
另外,我們將檢查密碼最少包含 6 個(gè)字符,最多包含 255 個(gè)字符。
和之前一樣,我們?cè)诰帉?xiě)實(shí)現(xiàn)前先為密碼復(fù)雜性需求編寫(xiě)測(cè)試。但是顯然,我們需要提供一個(gè)測(cè)試值的大集合用于驗(yàn)證實(shí)現(xiàn)。我們不是為每個(gè)測(cè)試用例創(chuàng)建一個(gè)單獨(dú)的測(cè)試,也不是創(chuàng)建一個(gè)循環(huán)來(lái)調(diào)用一系列的測(cè)試用例,我們將創(chuàng)建一個(gè)數(shù)據(jù)驅(qū)動(dòng)測(cè)試,它從數(shù)據(jù)庫(kù)中取出所需的數(shù)據(jù)。
測(cè)試視圖 (Test View) 窗口
首先我們定義一個(gè)名為 ChangePasswordTest() 的新測(cè)試。定義后,從菜單項(xiàng)測(cè)試->查看和創(chuàng)建測(cè)試(Test->View and Author Tests)為測(cè)試方法打開(kāi)測(cè)試視圖窗口,如圖 6 所示:
圖
6.
測(cè)試視圖
(
Test view
)
窗口
測(cè)試視圖窗口可用來(lái)運(yùn)行指定的測(cè)試和瀏覽測(cè)試的特定屬性。通過(guò)增加額外的列(右鍵單擊列頭并選擇添加/刪除列…),我們可以排序并根據(jù)偏好查看測(cè)試。有些列來(lái)自修飾測(cè)試的屬性。例如,添加 OwnerAttribute將在所有者列顯示測(cè)試的所有者。其它元數(shù)據(jù)屬性(如 DescriptionAttribute)也可以使用。這些屬性都可以在 Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名空間中找到。如果沒(méi)有顯式的屬性存在,那么我們可以使用自由形式的 TestPropertyAttribute來(lái)為特別的測(cè)試方法增加名-值對(duì)。
沒(méi)有對(duì)應(yīng)列的屬性可以在一個(gè)測(cè)試的屬性窗口中顯示(選擇一個(gè)測(cè)試,在右鍵上下文菜單中單擊屬性)。它包含了指定數(shù)據(jù)連接字符串和用于載入測(cè)試數(shù)據(jù)的表名的屬性。顯然,為了指定有效值,我們需要一個(gè)數(shù)據(jù)庫(kù)連接。
增加一個(gè)測(cè)試數(shù)據(jù)庫(kù)
從服務(wù)器資源管理器窗口,我們可以使用創(chuàng)建新的 SQL Server數(shù)據(jù)庫(kù)(Create new SQL Server Database) 菜單項(xiàng)。但是要小心這種方法,如果我們要在其它計(jì)算機(jī)上執(zhí)行測(cè)試的話,我們要保證在一臺(tái)服務(wù)器上創(chuàng)建數(shù)據(jù)庫(kù),其它可能執(zhí)行測(cè)試的計(jì)算機(jī)必須能夠訪問(wèn)該服務(wù)器 — 例如一臺(tái)用于構(gòu)建的計(jì)算機(jī)。
另外一個(gè)選擇是僅僅增加一個(gè)數(shù)據(jù)庫(kù)文件。使用項(xiàng)目->增加新項(xiàng)… (Project->Add new item...) 允許向項(xiàng)目插入一個(gè) SQL 數(shù)據(jù)庫(kù)文件。這種方法使測(cè)試數(shù)據(jù)和測(cè)試項(xiàng)目保持在一起。缺點(diǎn)是如果數(shù)據(jù)庫(kù)變得很大,我們就不想這么做,而寧可提供全局的數(shù)據(jù)源。
對(duì)于本項(xiàng)目中的數(shù)據(jù),我們創(chuàng)建一個(gè)名為 VSTSDemo.mdf的本地項(xiàng)目數(shù)據(jù)庫(kù)文件。為了向文件加入測(cè)試數(shù)據(jù),我們使用菜單工具->連接到數(shù)據(jù)庫(kù) (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
保存表后,我們可以將其打開(kāi),然后輸入不同的非法密碼,如下表所示。
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)系起來(lái)。從測(cè)試 InvalidPasswords的屬性窗口,我們填寫(xiě)數(shù)據(jù)連接字符串(Data Connection String) 和數(shù)據(jù)表名 (Data Table Name) 屬性。這樣做將使用附加的屬性 DataSourceAttribute和 DataTableNameAttribute更新測(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屬性,它指明了連接字符串、表名和訪問(wèn)順序。在這個(gè)清單中,我們使用數(shù)據(jù)庫(kù)文件名標(biāo)識(shí)數(shù)據(jù)庫(kù)。這樣的優(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è)試中訪問(wèn)跟測(cè)試環(huán)境關(guān)聯(lián)的數(shù)據(jù)。如圖 7 所示。
圖
7. TestContext
關(guān)聯(lián)
如圖 7 所示,TestContext提供了 TestDirectory和 TestName數(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ù),而不需要編寫(xiě)任何代碼。一旦需要額外的測(cè)試用例,我們需要做的就是向 LongonInfoTest 表增加關(guān)聯(lián)的數(shù)據(jù)。盡管我們可以創(chuàng)建兩個(gè)獨(dú)立的測(cè)試來(lái)使用單獨(dú)的表分別測(cè)試有效和無(wú)效數(shù)據(jù),這個(gè)特定的例子合并了這些測(cè)試來(lái)顯示稍微復(fù)雜的數(shù)據(jù)測(cè)試實(shí)例。
實(shí)現(xiàn)和重構(gòu)目標(biāo)方法
現(xiàn)在我們已經(jīng)有了測(cè)試,是時(shí)候?yàn)闇y(cè)試編寫(xiě)實(shí)現(xiàn)了。使用 C# 重構(gòu)工具,我們可以右鍵單擊 ChangePassword()方法調(diào)用,選擇菜單項(xiàng)GenerateMethodStub,然后對(duì)于生成的方法提供實(shí)現(xiàn),一旦我們成功地運(yùn)行了使用所有測(cè)試數(shù)據(jù)的測(cè)試,我們也可以開(kāi)始重構(gòu)代碼了,LogonInfo 類(lèi)的最終實(shí)現(xiàn)如清單 7 所示。
清單
7. LogonInfo
類(lèi)
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í)行,那些沒(méi)有被執(zhí)行。該功能如圖 8 所示。
圖
8.
突出顯示代碼覆蓋
圖 8 顯示了運(yùn)行所有單元測(cè)試后的代碼覆蓋的突出顯示情況。紅色突出顯示說(shuō)明了我們有產(chǎn)品代碼沒(méi)有運(yùn)行任何單元測(cè)試,這說(shuō)明我們編寫(xiě)這些代碼時(shí)未遵循 TDD 原則,即在編寫(xiě)實(shí)現(xiàn)前先提供測(cè)試。
初始化和清除測(cè)試
一般來(lái)說(shuō),測(cè)試類(lèi)不僅包含獨(dú)立的測(cè)試方法,還包含了不同的對(duì)測(cè)試進(jìn)行初始化和清除的方法。實(shí)際上,創(chuàng)建測(cè)試向?qū)г趧?chuàng)建 VSTSDemo.Test 項(xiàng)目時(shí),將一些這樣的方法添加到類(lèi) LongonInfoTest 中,見(jiàn)清單 8。
清單
8.
最終的
LogonInfoTest
類(lèi)
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è)置和清除的方法分別由屬性 TestInitializeAttribute和 TestCleanupAttribute修飾。在每個(gè)這樣的方法中,我們可以加入額外的代碼,它們將會(huì)在每個(gè)測(cè)試前或者測(cè)試后運(yùn)行。這意味著在每次對(duì)應(yīng)于 LongonInfoTest 表的記錄的 ChangePasswordTest()執(zhí)行前,Initialize() 和 Cleanup() 都會(huì)被執(zhí)行,每次 NullUserIdInConstructor和 EmptyUserIdInConstructor執(zhí)行時(shí)也會(huì)發(fā)生同樣的情況。這樣的方法可以用于向數(shù)據(jù)庫(kù)中插入默認(rèn)的數(shù)據(jù),然后在測(cè)試完成時(shí)清除插入的數(shù)據(jù)。例如,我們可以做到在 Initialize()中開(kāi)始一個(gè)事務(wù),然后在清除時(shí)回滾同一個(gè)事務(wù),這樣一來(lái),如果測(cè)試方法使用相同的連接時(shí),數(shù)據(jù)狀態(tài)會(huì)在每次測(cè)試執(zhí)行完成時(shí)恢復(fù)原狀。類(lèi)似地,測(cè)試文件也可以這樣處理。
在調(diào)試期間,TestCleanupAttribute修飾的方法可能由于調(diào)試器在清除的代碼執(zhí)行前終止運(yùn)行。由于這個(gè)原因,最好在設(shè)置測(cè)試期間檢查清除情況,并在需要時(shí)在設(shè)置測(cè)試前執(zhí)行清除代碼。關(guān)于初始化和清除的其它可用的測(cè)試屬性有 AssemblyInitializeAttribute/AssemblyCleanupAttribute和 ClassInitializeAttribute/ClassCleanupAttribute。程序集相關(guān)的屬性對(duì)整個(gè)程序集運(yùn)行一次,而類(lèi)相關(guān)的屬性對(duì)一個(gè)特定的測(cè)試類(lèi)的加載運(yùn)行一次。
最佳實(shí)踐
在結(jié)束前我們回顧幾種單元測(cè)試的最佳實(shí)踐。首先,TDD 是非常有價(jià)值的實(shí)踐。在所有現(xiàn)有的開(kāi)發(fā)方法中,TDD 可能是多年來(lái)根本上改進(jìn)開(kāi)發(fā)且投資成本最小的一種。每個(gè) QA 工程師都會(huì)告訴您,開(kāi)發(fā)人員在沒(méi)有相應(yīng)的測(cè)試前不會(huì)寫(xiě)出成功的軟件。有了 TDD,實(shí)踐是在實(shí)現(xiàn)前編寫(xiě)測(cè)試,并且理想情況是,編寫(xiě)的測(cè)試可以成為無(wú)需人工參與執(zhí)行的構(gòu)建腳本的一部分。需要訓(xùn)練來(lái)開(kāi)始養(yǎng)成習(xí)慣,但一旦建立習(xí)慣后,不使用 TDD 方法編碼就像開(kāi)車(chē)時(shí)不系安全帶一樣。
對(duì)于測(cè)試本身,有一些額外的原則可以幫助成功進(jìn)行測(cè)試:
? |
避免測(cè)試產(chǎn)生依賴性,這樣測(cè)試需要按照特定的順序執(zhí)行。每個(gè)測(cè)試都應(yīng)該是自治的。
|
? |
使用測(cè)試初始化代碼驗(yàn)證測(cè)試清除已經(jīng)成功執(zhí)行,如果沒(méi)有則在執(zhí)行測(cè)試前重新執(zhí)行清除。
|
? |
在編寫(xiě)任何產(chǎn)品代碼的實(shí)現(xiàn)前編寫(xiě)測(cè)試。
|
? |
對(duì)于產(chǎn)品代碼中的每個(gè)類(lèi)創(chuàng)建一個(gè)測(cè)試類(lèi)。這樣可以簡(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) 來(lái)測(cè)試接口。模擬對(duì)象通常在需要驗(yàn)證 API 符合所需功能的測(cè)試項(xiàng)目中實(shí)現(xiàn)。
|
? |
在繼續(xù)創(chuàng)建新的測(cè)試前驗(yàn)證所有測(cè)試運(yùn)行成功。這樣可以保證在破壞代碼后立刻進(jìn)行修正。
|
? |
可以最大化無(wú)需人工參與執(zhí)行的測(cè)試代碼。在依賴于手工測(cè)試前,必須完全肯定無(wú)法采用合理的無(wú)需人工參與執(zhí)行的測(cè)試方案。
|
小結(jié)
總的來(lái)說(shuō),VSTS 的單元測(cè)試功能本身很好理解。而且盡管本文沒(méi)有提到,它還可以通過(guò)自定義執(zhí)行引擎進(jìn)行擴(kuò)展。此外,它包含了代碼覆蓋分析的功能,這對(duì)于評(píng)價(jià)測(cè)試的全面性非常有用。通過(guò)使用 VSTS,您可以將測(cè)試數(shù)目和 bug 數(shù)目或編寫(xiě)的代碼數(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í)的功能。通過(guò)開(kāi)始實(shí)踐對(duì)代碼進(jìn)行單元測(cè)試,您會(huì)為產(chǎn)品的整個(gè)生命期建立一套寶貴的測(cè)試集。Team Test 通過(guò)與 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。現(xiàn)在他正在撰寫(xiě)另外一本有關(guān) C# 的書(shū),Essential C# (Addison Wesley)。不使用計(jì)算機(jī)時(shí),他會(huì)陪伴家人,進(jìn)行戶外運(yùn)動(dòng),或者進(jìn)行環(huán)球旅行。Mark Michaelis 住在 Spokane, WA。您可以通過(guò) mark@michaelis.net 和他聯(lián)系或者訪問(wèn)他的網(wǎng)絡(luò)日志:http://mark.michaelis.net。
轉(zhuǎn)到原英文頁(yè)面
翻譯者Luke是微軟公司的軟件工程師,習(xí)慣使用C++和C#開(kāi)發(fā)應(yīng)用程序。閑暇時(shí)間他喜歡音樂(lè),旅游和懷舊游戲,并且愿意幫助MSDN翻譯更多的文章和其他開(kāi)發(fā)者共享。可以通過(guò)ecaijw@msn.com聯(lián)系他。