演練:使用 Visual Studio Team Test 進(jìn)行單元測試
發(fā)布日期: 5/24/2005 | 更新日期: 5/24/2005
Mark Michaelis
Itron Corporation
摘要:本演練通過測試驅(qū)動(dòng)開發(fā) (TDD) 和先測試-后編碼 (test-then-code) 的方法學(xué)習(xí)單元測試。
本頁內(nèi)容
簡介
最新發(fā)布的 Visual Studio Test System (VSTS) 包含了一套用于 Visual Studio Team Test 的完整功能。Team Test 是 Visual Studio 集成的單元測試框架,它支持:
? |
測試方法存根 (stub) 的代碼生成。
|
? |
在 IDE 中運(yùn)行測試。
|
? |
合并從數(shù)據(jù)庫中加載的測試數(shù)據(jù)。
|
? |
測試運(yùn)行完成后,進(jìn)行代碼覆蓋分析。
|
另外,Team Test 包含了一套測試功能,可以同時(shí)支持開發(fā)人員和測試人員。
在本文中,我們準(zhǔn)備演練如何創(chuàng)建Team Test 的單元測試。我們從一個(gè)簡單的示例程序集開始,然后在該程序集中生成單元測試方法存根。這樣可以為Team Test 和單元測試的新手讀者提供基本的語法和代碼,同時(shí)也很好地介紹了如何快速建立測試項(xiàng)目的結(jié)構(gòu)。然后,我們轉(zhuǎn)到使用測試驅(qū)動(dòng)開發(fā) (test driven development, TDD) 方法,即在寫產(chǎn)品代碼前先寫單元測試。
Team Test的一個(gè)關(guān)鍵特點(diǎn)是從數(shù)據(jù)庫中加載測試數(shù)據(jù),然后將其用于測試方法。在演示基本的單元測試后,我們描述如何創(chuàng)建測試數(shù)據(jù)并集成到測試中。
本文中使用的示例項(xiàng)目包含一個(gè) LongonInfo 類,它封裝了與登錄相關(guān)的數(shù)據(jù)(例如用戶名和密碼)以及一些關(guān)于數(shù)據(jù)的簡單的驗(yàn)證規(guī)則。最終的類如下圖 1 所示。
圖
1.
最終的
LogonInfo
類
請(qǐng)注意所有的測試代碼位于一個(gè)單獨(dú)的項(xiàng)目。這是有道理的,產(chǎn)品代碼應(yīng)該盡可能少的受測試代碼影響,所以我們不想在產(chǎn)品代碼的程序集中嵌入測試代碼。
開始
首先,我們創(chuàng)建一個(gè)名為“VSTSDemo”的類庫項(xiàng)目。默認(rèn)情況下,為方案創(chuàng)建目錄(Create directory for solution) 復(fù)選框被選中。保留此選項(xiàng)可以使我們?cè)?VSTSDemo 項(xiàng)目的同一層目錄創(chuàng)建測試項(xiàng)目。相反,如果不選中此選項(xiàng),Visual Studio 2005 會(huì)將測試項(xiàng)目放在 VSTSDemo 項(xiàng)目的子目錄中。測試項(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ù):userId 和 password。一旦構(gòu)造函數(shù)的簽名被聲明,我們就可以為構(gòu)造函數(shù)生成測試。
圖
2.
LongonInfo
構(gòu)造函數(shù)的上下文菜單的“創(chuàng)建測試
…
”
(
Create Tests...
)
菜單項(xiàng)
創(chuàng)建測試
在開始編寫 LogonInfo的任何實(shí)現(xiàn)之前,我們遵循 TDD 實(shí)踐的規(guī)則,首先編寫測試。TDD 在Team Test 中并不是必需的,但最好在本文的剩余部分遵循 TDD。右鍵單擊 LogonInfo()構(gòu)造函數(shù),然后選擇“創(chuàng)建測試…”菜單項(xiàng)(如圖 2 所示)。這樣會(huì)出現(xiàn)一個(gè)對(duì)話框,可以在不同的項(xiàng)目中生成單元測試(如圖 3 所示)。默認(rèn)情況下,項(xiàng)目設(shè)置的輸出 (Output) 選項(xiàng)是一個(gè)新的 Visual Basic 項(xiàng)目,但是也可以選擇 C# 和 C++ 測試項(xiàng)目。在本文中,我們選擇 Visual C#,然后單擊 OK 按鈕,接著輸入項(xiàng)目名 VSTSDemo.Test。測試項(xiàng)目名稱。
圖
3.
生成單元測試對(duì)話框
生成的測試項(xiàng)目包含四個(gè)與測試相關(guān)的文件。
AuthoringTest.txt
|
提供關(guān)于創(chuàng)建測試的說明,包括向項(xiàng)目增加其他測試的說明。
|
LogonInfoTest.cs
|
包含了用于測試 LogonInfo()的生成測試,以及測試初始化和測試清除的方法。
|
ManualTest1.mht
|
提供了一個(gè)模板,可以填入手工測試的指令。
|
UnitTest1.cs
|
一個(gè)空的單元測試類架構(gòu),用于放入另外的單元測試。
|
因?yàn)槲覀儾淮蛩銓?duì)該項(xiàng)目進(jìn)行手工測試,并且由于已經(jīng)有了一個(gè)單元測試文件,我們將刪除 ManualTest1.mht 和 UnitTest1.cs。
除了一些默認(rèn)的文件,生成的測試項(xiàng)目還包含了對(duì) Microsoft.VisualStudio.QualityTools.UnitTestFramework和 VSTSDemo 項(xiàng)目的引用。前者是測試引擎運(yùn)行單元測試需要依賴的測試框架程序集,后者是對(duì)我們需要測試的目標(biāo)程序集的項(xiàng)目引用。
默認(rèn)情況下,生成的測試方法是包含以下實(shí)現(xiàn)的占位符:
清單
1.
生成的測試方法:
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ù)測試目標(biāo)的方法類型和簽名不同而有所不同。例如,向?qū)?huì)為私有成員函數(shù)的測試生成反射代碼。在這種特別的情況下,我們需要專門用于公有構(gòu)造函數(shù)測試的代碼。
關(guān)于Team Test ,有兩個(gè)重要的特性。首先,作為測試的方法由 TestMethodAttribute屬性指定,另外,包含測試方法的類有 TestClassAttribute屬性。這些屬性都可以在 Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名空間中找到。Team Test 使用反射機(jī)制在測試程序集中搜索所有由 TestClass修飾的類,然后查找由 TestMethodAttribute修飾的方法來決定執(zhí)行的內(nèi)容。另外一個(gè)重要的由執(zhí)行引擎而不是編譯器驗(yàn)證的標(biāo)準(zhǔn)是,測試方法的簽名必須是無參數(shù)的實(shí)例方法。因?yàn)榉瓷渌阉?TestMethodAttribute,所以測試方法可以使用任意的名字。
測試方法 ConstructorTest()首先實(shí)例化目標(biāo) LongonInfo 類,然后斷言測試是非決定性的(使用Assert.Inconclusive())。當(dāng)測試運(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ò)誤在單元測試框架中非常普遍。
因?yàn)?UserID 和 Password 的實(shí)例域還沒有創(chuàng)建,我們需要回頭將其添加到 LogonInfo類中,以便VSTTDemo.Test 項(xiàng)目可以編譯。
即使我們還沒有一個(gè)有效的實(shí)現(xiàn),讓我們開始運(yùn)行測試。如果我們遵循 TDD 方法,我們就應(yīng)該直到測試證明我們需要這樣的代碼時(shí)才去編寫產(chǎn)品代碼。我們僅在建立項(xiàng)目結(jié)構(gòu)時(shí)違背此原則,但是一旦項(xiàng)目建立后,就可以容易地始終遵循 TDD 方法。
運(yùn)行測試
要運(yùn)行項(xiàng)目中的所有測試,只需要運(yùn)行測試項(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)行測試。
這時(shí)出現(xiàn)測試結(jié)果窗口,列出項(xiàng)目中的所有測試。因?yàn)槲覀兊捻?xiàng)目只包含一個(gè)測試,因此只列出了一個(gè)測試。開始的時(shí)候,測試會(huì)處于掛起的狀態(tài),但是一旦測試完成,結(jié)果將是我們意料中的失敗(如圖 4 所示)。
圖
4.
執(zhí)行所有測試后的測試結(jié)果窗口
圖 4 顯示了測試結(jié)果 (Test Results) 窗口。這個(gè)特別的屏幕快照除了默認(rèn)的列外,還顯示了錯(cuò)誤信息。您可以在列頭上單擊右鍵并選擇菜單項(xiàng)增加/刪除列…以增加或者刪除列。
如果要查看測試的額外細(xì)節(jié),我們可以選定測試并雙擊,打開“ConstructorTest[Results]”窗口,如圖 5 所示。
圖
5.
詳細(xì)的測試
ConstructorTest [
Results
]
窗口
另外,我們可以右鍵單擊單個(gè)測試,然后選擇打開測試(Open Test) 菜單項(xiàng),進(jìn)入測試代碼。因?yàn)槲覀円呀?jīng)知道問題在于 LogonInfo 構(gòu)造函數(shù)的實(shí)現(xiàn),我們可以去那里編寫初始化 UserID 和 Password 字段的代碼,使用傳入的參數(shù)對(duì)它們進(jìn)行初始化。重新運(yùn)行測試以驗(yàn)證測試現(xiàn)在可以通過。
檢查異常
下一步是創(chuàng)建 LongonInfo 類,以提供對(duì) UserID 和 password 的一些驗(yàn)證。不幸的是,UserID和 Password 字段是公共的,這意味著它們沒有提供任何封裝來確保它們有效。但是在我們將其轉(zhuǎn)換為屬性并提供驗(yàn)證前,讓我們編寫一些測試來驗(yàn)證任何實(shí)現(xiàn)的結(jié)果都是正確的。
我們首先來編寫一個(gè)測試,防止空值 (null) 或空字符串賦值給 UserID。預(yù)期結(jié)果是,如果空值傳送給構(gòu)造函數(shù),會(huì)引發(fā)一個(gè) ArgumentException異常。測試代碼如清單 3 所示。
清單
3.
使用
ExpectedExceptionAttribute
對(duì)異常情況進(jìn)行測試
[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 代碼塊的顯式測試。不過,兩個(gè)測試都包含另外一個(gè)屬性 ExpectedException,它接受一個(gè)類型參數(shù),以及一個(gè)可選的錯(cuò)誤信息,用于在沒有引發(fā)異常時(shí)顯示。當(dāng)這個(gè)單元測試執(zhí)行時(shí),測試框架會(huì)顯式地監(jiān)視引發(fā)的 ArgumentException異常,如果方法沒有引發(fā)這個(gè)異常,測試將失敗。運(yùn)行這些測試會(huì)證明我們還沒有對(duì) UserID 做任何驗(yàn)證檢查;因此,測試會(huì)失敗,因?yàn)闆]有引發(fā)預(yù)期的異常。
有了失敗的測試,現(xiàn)在可以回到產(chǎn)品代碼進(jìn)行更新來提供測試需要檢查的功能。在這個(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)行測試來驗(yàn)證實(shí)現(xiàn)是正確的。我們運(yùn)行所有的三個(gè)測試來驗(yàn)證 UserID 字段轉(zhuǎn)換為屬性的重構(gòu)過程沒有產(chǎn)生任何意外的錯(cuò)誤。單元測試的真正價(jià)值在代碼修改的時(shí)候才真正有所體現(xiàn)。一套單元測試可以保證我們?cè)诰S護(hù)和改進(jìn)代碼的時(shí)候沒有破壞代碼。
從數(shù)據(jù)庫中加載測試數(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ù)雜性需求編寫測試。但是顯然,我們需要提供一個(gè)測試值的大集合用于驗(yàn)證實(shí)現(xiàn)。我們不是為每個(gè)測試用例創(chuàng)建一個(gè)單獨(dú)的測試,也不是創(chuàng)建一個(gè)循環(huán)來調(diào)用一系列的測試用例,我們將創(chuàng)建一個(gè)數(shù)據(jù)驅(qū)動(dòng)測試,它從數(shù)據(jù)庫中取出所需的數(shù)據(jù)。
測試視圖 (Test View) 窗口
首先我們定義一個(gè)名為 ChangePasswordTest() 的新測試。定義后,從菜單項(xiàng)測試->查看和創(chuàng)建測試(Test->View and Author Tests)為測試方法打開測試視圖窗口,如圖 6 所示:
圖
6.
測試視圖
(
Test view
)
窗口
測試視圖窗口可用來運(yùn)行指定的測試和瀏覽測試的特定屬性。通過增加額外的列(右鍵單擊列頭并選擇添加/刪除列…),我們可以排序并根據(jù)偏好查看測試。有些列來自修飾測試的屬性。例如,添加 OwnerAttribute將在所有者列顯示測試的所有者。其它元數(shù)據(jù)屬性(如 DescriptionAttribute)也可以使用。這些屬性都可以在 Microsoft.VisualStudio.QualityTools.UnitTesting.Framework 命名空間中找到。如果沒有顯式的屬性存在,那么我們可以使用自由形式的 TestPropertyAttribute來為特別的測試方法增加名-值對(duì)。
沒有對(duì)應(yīng)列的屬性可以在一個(gè)測試的屬性窗口中顯示(選擇一個(gè)測試,在右鍵上下文菜單中單擊屬性)。它包含了指定數(shù)據(jù)連接字符串和用于載入測試數(shù)據(jù)的表名的屬性。顯然,為了指定有效值,我們需要一個(gè)數(shù)據(jù)庫連接。
增加一個(gè)測試數(shù)據(jù)庫
從服務(wù)器資源管理器窗口,我們可以使用創(chuàng)建新的 SQL Server數(shù)據(jù)庫(Create new SQL Server Database) 菜單項(xiàng)。但是要小心這種方法,如果我們要在其它計(jì)算機(jī)上執(zhí)行測試的話,我們要保證在一臺(tái)服務(wù)器上創(chuàng)建數(shù)據(jù)庫,其它可能執(zhí)行測試的計(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ù)庫文件。這種方法使測試數(shù)據(jù)和測試項(xiàng)目保持在一起。缺點(diǎn)是如果數(shù)據(jù)庫變得很大,我們就不想這么做,而寧可提供全局的數(shù)據(jù)源。
對(duì)于本項(xiàng)目中的數(shù)據(jù),我們創(chuàng)建一個(gè)名為 VSTSDemo.mdf的本地項(xiàng)目數(shù)據(jù)庫文件。為了向文件加入測試數(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
保存表后,我們可以將其打開,然后輸入不同的非法密碼,如下表所示。
Humperdink
|
P@w0d
|
False
|
IMontoya
|
p@ssword
|
False
|
Inigo.Montoya
|
P@ssw0rd
|
False
|
Wesley
|
Password
|
False
|
將數(shù)據(jù)與測試關(guān)聯(lián)
一旦完成表的創(chuàng)建,我們需要將其與測試 InvalidPasswords()聯(lián)系起來。從測試 InvalidPasswords的屬性窗口,我們填寫數(shù)據(jù)連接字符串(Data Connection String) 和數(shù)據(jù)表名 (Data Table Name) 屬性。這樣做將使用附加的屬性 DataSourceAttribute和 DataTableNameAttribute更新測試。最終的方法 ChangePasswordTest()在清單 6 中顯示。
清單
6.
用于數(shù)據(jù)驅(qū)動(dòng)測試的測試代碼
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)是該文件和測試項(xiàng)目一起遷移,假設(shè)它可能會(huì)被移動(dòng)到一個(gè)相對(duì)的路徑。
第二個(gè)注意的地方是 TestContext.DataRow調(diào)用。TestContext是在我們運(yùn)行創(chuàng)建測試向?qū)r(shí)由生成器提供的屬性,它在運(yùn)行時(shí)由測試執(zhí)行引擎自動(dòng)賦值,這樣我們就可以在測試中訪問跟測試環(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)用一次。這就使測試代碼使用運(yùn)行中的測試的數(shù)據(jù),而且對(duì)插入 LongonInfoTest 表的每條記錄重復(fù)執(zhí)行測試。如果表包含四條記錄,那么測試將會(huì)分別執(zhí)行四次。
使用這樣的數(shù)據(jù)驅(qū)動(dòng)測試方法,可以很容易的提供額外的測試數(shù)據(jù),而不需要編寫任何代碼。一旦需要額外的測試用例,我們需要做的就是向 LongonInfoTest 表增加關(guān)聯(lián)的數(shù)據(jù)。盡管我們可以創(chuàng)建兩個(gè)獨(dú)立的測試來使用單獨(dú)的表分別測試有效和無效數(shù)據(jù),這個(gè)特定的例子合并了這些測試來顯示稍微復(fù)雜的數(shù)據(jù)測試實(shí)例。
實(shí)現(xiàn)和重構(gòu)目標(biāo)方法
現(xiàn)在我們已經(jīng)有了測試,是時(shí)候?yàn)闇y試編寫實(shí)現(xiàn)了。使用 C# 重構(gòu)工具,我們可以右鍵單擊 ChangePassword()方法調(diào)用,選擇菜單項(xiàng)GenerateMethodStub,然后對(duì)于生成的方法提供實(shí)現(xiàn),一旦我們成功地運(yùn)行了使用所有測試數(shù)據(jù)的測試,我們也可以開始重構(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.");
}
}
}
}
代碼覆蓋
單元測試的一個(gè)關(guān)鍵度量是決定在單元測試運(yùn)行時(shí)測試了多少代碼。該度量稱為代碼覆蓋,Team Test 包含了一個(gè)代碼覆蓋工具,可以詳細(xì)解釋被執(zhí)行代碼的百分率,并突出顯示哪些代碼被執(zhí)行,那些沒有被執(zhí)行。該功能如圖 8 所示。
圖
8.
突出顯示代碼覆蓋
圖 8 顯示了運(yùn)行所有單元測試后的代碼覆蓋的突出顯示情況。紅色突出顯示說明了我們有產(chǎn)品代碼沒有運(yùn)行任何單元測試,這說明我們編寫這些代碼時(shí)未遵循 TDD 原則,即在編寫實(shí)現(xiàn)前先提供測試。
初始化和清除測試
一般來說,測試類不僅包含獨(dú)立的測試方法,還包含了不同的對(duì)測試進(jìn)行初始化和清除的方法。實(shí)際上,創(chuàng)建測試向?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ì)測試進(jìn)行設(shè)置和清除的方法分別由屬性 TestInitializeAttribute和 TestCleanupAttribute修飾。在每個(gè)這樣的方法中,我們可以加入額外的代碼,它們將會(huì)在每個(gè)測試前或者測試后運(yùn)行。這意味著在每次對(duì)應(yīng)于 LongonInfoTest 表的記錄的 ChangePasswordTest()執(zhí)行前,Initialize() 和 Cleanup() 都會(huì)被執(zhí)行,每次 NullUserIdInConstructor和 EmptyUserIdInConstructor執(zhí)行時(shí)也會(huì)發(fā)生同樣的情況。這樣的方法可以用于向數(shù)據(jù)庫中插入默認(rèn)的數(shù)據(jù),然后在測試完成時(shí)清除插入的數(shù)據(jù)。例如,我們可以做到在 Initialize()中開始一個(gè)事務(wù),然后在清除時(shí)回滾同一個(gè)事務(wù),這樣一來,如果測試方法使用相同的連接時(shí),數(shù)據(jù)狀態(tài)會(huì)在每次測試執(zhí)行完成時(shí)恢復(fù)原狀。類似地,測試文件也可以這樣處理。
在調(diào)試期間,TestCleanupAttribute修飾的方法可能由于調(diào)試器在清除的代碼執(zhí)行前終止運(yùn)行。由于這個(gè)原因,最好在設(shè)置測試期間檢查清除情況,并在需要時(shí)在設(shè)置測試前執(zhí)行清除代碼。關(guān)于初始化和清除的其它可用的測試屬性有 AssemblyInitializeAttribute/AssemblyCleanupAttribute和 ClassInitializeAttribute/ClassCleanupAttribute。程序集相關(guān)的屬性對(duì)整個(gè)程序集運(yùn)行一次,而類相關(guān)的屬性對(duì)一個(gè)特定的測試類的加載運(yùn)行一次。
最佳實(shí)踐
在結(jié)束前我們回顧幾種單元測試的最佳實(shí)踐。首先,TDD 是非常有價(jià)值的實(shí)踐。在所有現(xiàn)有的開發(fā)方法中,TDD 可能是多年來根本上改進(jìn)開發(fā)且投資成本最小的一種。每個(gè) QA 工程師都會(huì)告訴您,開發(fā)人員在沒有相應(yīng)的測試前不會(huì)寫出成功的軟件。有了 TDD,實(shí)踐是在實(shí)現(xiàn)前編寫測試,并且理想情況是,編寫的測試可以成為無需人工參與執(zhí)行的構(gòu)建腳本的一部分。需要訓(xùn)練來開始養(yǎng)成習(xí)慣,但一旦建立習(xí)慣后,不使用 TDD 方法編碼就像開車時(shí)不系安全帶一樣。
對(duì)于測試本身,有一些額外的原則可以幫助成功進(jìn)行測試:
? |
避免測試產(chǎn)生依賴性,這樣測試需要按照特定的順序執(zhí)行。每個(gè)測試都應(yīng)該是自治的。
|
? |
使用測試初始化代碼驗(yàn)證測試清除已經(jīng)成功執(zhí)行,如果沒有則在執(zhí)行測試前重新執(zhí)行清除。
|
? |
在編寫任何產(chǎn)品代碼的實(shí)現(xiàn)前編寫測試。
|
? |
對(duì)于產(chǎn)品代碼中的每個(gè)類創(chuàng)建一個(gè)測試類。這樣可以簡化測試的組織,并可以容易地選擇在何處放置每個(gè)測試。
|
? |
使用 Visual Studio 生成初始化的測試項(xiàng)目。這樣可以大大減少手工設(shè)置測試項(xiàng)目并與產(chǎn)品項(xiàng)目關(guān)聯(lián)的步驟。
|
? |
避免創(chuàng)建其他依賴計(jì)算機(jī)的測試,例如依賴特定的目錄路徑的測試。
|
? |
創(chuàng)建模擬對(duì)象 (mock object) 來測試接口。模擬對(duì)象通常在需要驗(yàn)證 API 符合所需功能的測試項(xiàng)目中實(shí)現(xiàn)。
|
? |
在繼續(xù)創(chuàng)建新的測試前驗(yàn)證所有測試運(yùn)行成功。這樣可以保證在破壞代碼后立刻進(jìn)行修正。
|
? |
可以最大化無需人工參與執(zhí)行的測試代碼。在依賴于手工測試前,必須完全肯定無法采用合理的無需人工參與執(zhí)行的測試方案。
|
小結(jié)
總的來說,VSTS 的單元測試功能本身很好理解。而且盡管本文沒有提到,它還可以通過自定義執(zhí)行引擎進(jìn)行擴(kuò)展。此外,它包含了代碼覆蓋分析的功能,這對(duì)于評(píng)價(jià)測試的全面性非常有用。通過使用 VSTS,您可以將測試數(shù)目和 bug 數(shù)目或編寫的代碼數(shù)量進(jìn)行關(guān)聯(lián)比較。這為項(xiàng)目的運(yùn)行狀況提供了很好的指標(biāo)。
本文介紹了Team Test 產(chǎn)品中的基本單元測試功能,也探討了關(guān)于數(shù)據(jù)驅(qū)動(dòng)測試的一些更加高級(jí)的功能。通過開始實(shí)踐對(duì)代碼進(jìn)行單元測試,您會(huì)為產(chǎn)品的整個(gè)生命期建立一套寶貴的測試集。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。現(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)系他。