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