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