一引言
本文旨在向你解釋創建如何使用Visual Studio 進行單元測試更具體地說我不想泛泛地談論單元測試的有關概念而是想專注於討論當構建ASPNET MVC Web應用程序工程時如何在測試驅動開發環境下構建一個特定類型的單元測試
其實並非所有的單元測試都是優秀的TDD測試要想在測試驅動開發中應用單元測試你必須能夠執行以非常快的速度執行單元測試然而並非所有的單元測試都能滿足這個要求
例如Visual Studio針對ASPNET網站提供了一種特定類型的單元測試支持你必須在IIS或開發web服務器上下文中執行這個類型的單元測試但是當你進行測試驅動開發時這並不是一個適當類型的單元測試因為這個類型的單元測試速度太慢了
在本文中我想向你展示構建用於測試驅動開發的單元測試的詳細過程我將詳細地向你描述使用Visual Studio 單元測試框架的有關細節此外我還要討論若干高級題目例如測試私有方法和如何從命令行執行測試等等
【注意】本文中所描述的大多數特征為Visual Studio Professional Edition所支持但遺憾的是這些特征卻並不為Visual Web Developer所支持因此如果讀者想了解關於Visual Studio 中每種版本對於單元測試特征的支持詳情請參考網址us/library/bbaspx
二快速創建一個ASPNET MVC Web應用程序示例
首先讓我們創建一個新的ASPNET MVC Web應用程序工程並且創建一個相應的測試工程這一步是非常容易的當你創建一個新的ASPNET MVC Web應用程序工程時系統會隨後提示你是否創建一個新的Visual Studio測試工程如圖所示只要你保持圖頂部的單選按鈕(即缺省的選項)那麼你會看到一個新的測試工程自動地添加到你的方案上
圖—創建一個新的ASPNET MVC Web應用程序工程和相應的單元測試工程
現在的問題是既然你有一個測試工程那麼你該如何使用這個測試工程呢?
當你創建一個新的ASPNET MVC應用程序時工程包括一個名字為HomeController的控制器這個控制器有兩個名字分別為Index()和About()的缺省方法相應於該HomeController工程提供了一個文件名字為HomeControlleterTest的測試工程這個測試文件包含兩個測試方法分別為Index()和About()
默認情況下Index()和About()這兩個測試方法內容為空(如圖所示)接下來你可以在這些方法中添加你的測試邏輯
圖—系統自動生成的測試工程中的About()測試方法為空
假設我們要構建一個在線存儲系統比如說你想創建一個Details頁面用於顯示一個特定產品的細節信息然後你要把一個包含ProductId的查詢字符串傳遞到這個Details頁面並且要實現從數據庫中檢索產品細節信息而且要把此信息顯示到頁面上
在良好的測試驅動開發實踐中在真正編碼之前你首先需要編寫一個測試你不是先編寫任何應用程序代碼而是先編寫相應於該代碼的測試為了創建一個成功的Details頁面必須滿足下列測試要求
()如果沒有把一個ProductId傳遞到該頁面則應該拋出一個異常
()該ProductId應該用於從數據庫中檢索一個產品
()如果不能從數據庫中檢索出一個相匹配的產品那麼應該拋出一個異常
()Details視圖應該能夠順利生成
()Product數據應該被賦值給Details視圖的ViewData結構
接下來我們將首先實現測試代碼的編寫根據前面的第一條測試要求如果沒有把一個ProductId傳遞到該頁面則應該拋出一個異常我們需要把一個新的單元測試添加到我們的測試工程右擊你的測試工程的Controllers文件夾選擇添加→新的測試然後選擇單元測試模板(見圖)並且命名此該新的單元測試為ProductControllerTest
圖—添加一個新的單元測試
在此請諸位注意我們是如何創建一個新的單元測試的因為存在多種方式可以錯誤地添加一個單元測試例如如果你右擊Controllers文件夾並且選擇添加→新的測試那麼你會看到一個單元測試向導這個向導將生成一個單元測試此測試將運行於一個web服務器上下文中但是這並不是我們想實現的如果你看到如圖所示的對話框那麼你要提醒自己你正在以一種錯誤的方式試圖添加一個MVC單元測試
圖—無論何時看到這樣一個對話框請點擊Cancel按鈕!
默認情況下該ProductControllerTest將包含如列表所示的唯一的一個測試方法
列表—系統自動生成的最初的文件ProductControllerTestcs
[TestMethod]
public void TestMethod()
{
//
// TODO: 在此添加測試邏輯
//…………
}
現在我們想修改這個測試方法以便它能夠測試是否拋出一個異常—當Details頁面要求的ProductId參數不能滿足時於是我們創建如列表所示的正確測試
列表—修改後的文件ProductControllerTestcs
[TestMethod]
[ExpectedException(typeof(ArgumentNullException) Exception no ProductId)]
public void Details_NoProductId_ThrowException()
{
ProductController controller = new ProductController();
controllerDetails(null);
}
現在讓我來解釋一下列表中的有關測試編碼該方法使用兩個屬性加以修飾其中第一個屬性[TestMethod]標識此方法為一個測試方法第二個屬性[ExpectedException]則建立針對於該測試的一個期望如果執行該測試方法的過程中不拋出一個ArgumentNullException型異常那麼該測試失敗我們之所以想使該測試拋出一個異常是因為我們想實現當Details頁面所要求的ProductId參數不能滿足時拋出一個異常
接下來測試方法正文部分包含了兩個語句其中第一個語句創建了ProductController類的一個實例第二個語句調用控制器的Details()方法
值得注意的是目前在我們的MVC應用程序中我們還沒有創建這個ProductController類因此這個測試不會成功執行但是這正是我們所期望實現的很顯然當使用測試驅動思想進行開發時這正體現了這種開發思想的重要特征首先你要編寫一個將會失敗的測試然後你再編寫代碼來進一步修改這個測試再運行測試再修改……直到通過測試
因此讓我們運行上面的這個測試—而且我們將會得到期望的失敗結果注意到在代碼編輯器窗口的頂部應該有一個包含有兩個按鈕的工具欄這兩個按鈕用於運行測試其中第一個按鈕支持在當前上下文中運行當前測試而第二個按鈕能夠在當前方案中運行所有的測試(見圖)
圖—使用Visual Studio 測試工具欄
現在我們來詳細分析一下點擊這兩個按鈕有什麼不同效果在當前上下文運行測試將執行不同的測試依賴於你的鼠標光標在代碼編輯器窗口中所在的位置如果你的鼠標光標位於一個特定的測試方法中那麼將僅僅執行此方法如果你的鼠標光標位於整個測試類中那麼該測試類中所有的測試都將被執行如果當前焦點位於測試結果窗口中那麼將執行所有的測試(有關細節請參考unittestingfeaturesinorcaspartaspx)
實際上我建議你應該總是努力避免使用鼠標點擊按鈕的方式一方面點擊按鈕速度太慢另一方面測試驅動開發要求所有執行測試過程必須相當迅速因此我推薦你使用下列這些組合鍵來執行測試
CtrlRA—運行方案中的所有測試
CtrlRT—運行當前上下文中的所有測試
CtrlRN—運行當前命名空間中的所有測試
CtrlRC—運行當前類中的所有測試
CtrlRCtrlA—調試方案中的所有測試
CtrlRCtrlT—調試當前上下文中的所有測試
CtrlRCtrlN—調試當前命名空間中的所有測試
CtrlRCtrlC—調試當前類中的所有測試
如果你使用CtrlRA組合鍵運行我們剛剛創建的測試方法那麼它將失敗該測試甚至不會編譯成功因為我們還沒有創建ProductController類或一個Details()方法這正是我們接下來要做的
切換回到ASPNET MVC工程使用鼠標右擊Controllers文件夾然後選擇Add→New Item選擇Web類型並且選擇MVC控制器類把新的控制器命名為ProductController並且點擊Add按鈕(或僅僅按一下回車鍵)於是創建一個包括一個Index()方法的新的控制器
現在我們想編寫盡可能少的代碼僅使我們的單元測試運行通過就行列表中的ProductController類將能夠通過我們的單元測試
列表—ProductControllercs
using System;
using SystemCollectionsGeneric;
using SystemLinq;
using SystemWeb;
using SystemWebMvc;
namespace MvcApplicationControllers
{
public class ProductController : Controller
{
public void Details(int? ProductId)
{
throw new ArgumentNullException(ProductId);
}
}
}
在此列表中的類ProductController包含一個方法名字為Details()請注意在此我略去了當你創建一個新的控制器時默認生成的Index()方法於是上面的Details()方法總是拋出一個ArgumentNullException異常
在輸入列表中的代碼後按下鍵盤上的組合鍵CtrlRA(你不需要切換回測試工程運行測試)圖展示了我們的測試成功時的測試結果窗口
圖—成功通過測試的綠色對號提示
你可能會認為你不是瘋了吧?沒錯當前情況下我們的Details()方法總是拋出一個異常在此再次提醒你注意測試驅動開發的基本思想目前情況下你僅需專注於滿足你的測試要求就行以後的測試將迫使你進一步構建一個更為符合實際要求的控制器方法
三Visual Studio測試屬性
在上一節構建我們的測試時我們需要使用下列兩個屬性
[TestMethod]—用於把一個方法標記為一個測試方法當你運行你的測試時僅標記有這個屬性的方法才能夠運行
[TestClass]—用於把一個類標記為一個測試類當你運行你的測試時僅標記有這個屬性的類才能夠運行
當構建測試時你總是使用[TestMethod]和[TestClass]屬性然而還存在其它若干有用的(但是可選的)測試屬性例如你可以使用下列屬性對來建立和簡化你的測試
[AssemblyInitialize]和[AssemblyCleanup]—分別用於標記那些在一個程序集中的所有測試執行之前或之後要執行的方法
[ClassInitialize]和[ClassCleanup]—分別用於標記那些在一個類中的所有測試執行之前或之後要執行的方法
[TestInitialize]和[TestCleanup]—分別用於標記那些在一個特定的測試方法之前或之後要執行的方法
例如你可能想創建一個虛構的HttpContext並使之應用於你所有的測試方法中此時你可以在一個標記有[ClassInitialize]屬性的方法中建立該虛構的HttpContext然後在一個標記有[ClassCleanup]屬性的方法中釋放此虛構的HttpContext
此外還存在若干屬性你可以用於提供關於測試方法的額外信息當你操作成百上千的單元測試時你需要通過排序和過濾等方法來管理這些測試此時下面這些屬性就變得相當有用
[Owner]—指定一個測試方法的作者
[Description]—提供一個測試方法的描述
[Priority]—能夠使你為一個測試指定一個整數優先權
[TestProperty]—指定一個隨意的測試屬性
你可以在測試視圖窗口或測試列表編輯器中使用這些屬性來排序和過濾測試
最後還存在一個屬性可以支持你當運行一個測試時忽略一個特定的測試方法當你的一個測試出現問題並且你目前還不想處理該問題時這個屬性就變得相當有用的
? [Ignore]—支持你臨時性地禁用一個特定的測試你可以把這個屬性應用於一個測試方法或一個測試類之上
四 創建測試斷言
大多數情況下當時你編寫你的測試方法代碼時你都會使用Assert類提供的方法例如大多數測試方法中的最後一行代碼往往都使用Assert類來斷言一個測試必須滿足的條件從而最終使該測試順利通過
Assert類支持下列靜態方法
AreEqual—斷言兩個值是相等的
AreNotEqual—斷言兩個值不是相等的
AreNotSame—斷言兩個對象是不同的對象
AreSame—斷言兩個對象是相同的對象
Fail—斷言一個測試失敗
Inconclusive—斷言一個測試的結果是不確定的Visual Studio在它自動生成的方法中包括了這個斷言要求你自己去實現
IsFalse—斷言一個給定條件表達式返回值False
IsInstanceOfType—斷言一個給定對象是一個指定類型的實例
IsNotInstanceOfType—斷言一個給定對象不是一個指定類型的一個實例
IsNotNull—斷言一個對象不是一個Null值
IsNull—斷言一個對象為一個Null值
IsTrue—斷言一個給定條件表達式返回值True
ReplaceNullChars—在一個以\結尾的字符串中使用\\代替其中的Null字符
當上面任何一個Assert方法失敗時該Assert類將拋出一個AssertFailedException異常
例如假定你在編寫一個單元測試來測試一個方法此方法實現兩個數求和列表中的測試方法使用了一個Assert方法檢查是否被測試方法返回+相應的正確的結果
列表–CalculateTestcs
[TestMethod]
public void AddNumbersTest()
{
int result = CalculateAdd( );
AssertAreEqual(result + );
}
值得注意的是有一個特定的名為CollectionAssert的類用於測試與集合相關的斷言該CollectionAssert類支持下列靜態方法
AllItemsAreInstancesOfType—斷言一個集合中的每一項都屬於一個指定的類型
AllItemsAreNotNull—斷言一個集合中的每一項都非空
AllItemsAreUnique—斷言一個集合中的每一項都是唯一的
AreEqual—斷言兩個集合中的每一個對應項的值都相等
AreEquivalent—斷言兩個集合中的每一個對應項的值都相等(但是第一個集合中的項的順序可能與第二個集合中的項的順序不相匹配)
AreNotEqual—斷言兩個集合不是相等的
AreNotEquivalent—斷言兩個集合不是相等的
Contains—斷言一個集合包含一個指定的項
DoesNotContain—斷言一個集合不包含一個指定的項
IsNotSubsetOf—斷言一個集合不是另一個集合的一個子集
IsSubsetOf—斷言一個集合是另一個集合的一個子集
此外還存在一個特別的名為StringAssert的類專門用於實現有關於字符串的斷言該StringAssert類支持下列靜態方法
Contains—斷言一個字符串包含一個指定的子串
DoesNotMatch—斷言一個字符串不匹配一個指定的正規表達式
EndsWith—斷言一個字符串以一個指定的子串結束
Matches—斷言一個字符串匹配一個指定的正規表達式
StartsWith—斷言一個字符串以一個指定的子串開頭
最後你可以使用[ExpectedException]屬性來斷言一個測試方法應該拋出一個特定類型的異常在前面的例子中我們就使用了該ExpectedException屬性來測試是否一個NullProductId會致使一個控制器拋出一個ArgumentNullException類型的異常
五 從現有代碼生成測試
Visual Studio 支持你從現有代碼自動地生成單元測試為此你可以右擊一個類中的任何方法並且選擇Create Unit Tests…選項
圖—從現有代碼自動生成的一個單元測試
一般說來每一位測試驅動開發者都會不同程度地使用以前遺留(或別人提供)的現成的代碼所以如果你需要在現有代碼上添加單元測試的話那麼你可以利用這個選項來快速地創建必要的測試方法相應的基本代碼部分
【注意】關於使用這個方法添加單元測試目前尚存在一個BUG如果你在一個ASPNET MVC Web應用程序工程的一個類上使用這個選項那麼你會看到將打開一個單元測試向導但遺憾的是這個向導生成的單元測試是執行於一個web服務器的上下文環境下顯然這個類型的單元測試是不適合於測試驅動開發的因為它的執行需要花費太長的時間因此我推薦你僅當使用類庫工程時才使用本節中所描述的方法生成單元測試
六 測試私有方法屬性和域
當遵循良好的測試驅動開發思想進行開發時你應當測試你的所有的代碼包括你的應用程序中定義的私有方法那麼該如何測試你的測試工程中定義的私有方法呢?乍看起來問題似乎是不能從一個單元測試內部調用私有方法
針對上面這個問題存在兩種解決方案首先Visual Studio 可以生成一個類以暴露被測試類的所有私有類型的成員在Visual Studio 中你可以從代碼編輯器中右擊任何類然後選擇菜單選項創建私有訪問器(即Create Private Accessor)選擇這個菜單選項將生成一個新類借助於這個新類它能夠把所有的私有方法屬性和域暴露為公共類型的方法屬性和域
例如假定你想測試一個名字為Calculate的類中包含的一個名字為Subtract()的私有類型方法那麼你可以右擊這個類來生成一個訪問器(Accessor)見圖
圖—創建一個私有類型的訪問器(Accessor)
在你創建該訪問器後你可以把它應用於你的單元測試代碼中來測試該Subtract方法例如列表中提供的單元測試將測試是否該subtract方法返回–的正確結果
列表—CalculateTestcs(訪問器)
[TestMethod]
public void SubtractTest()
{
int result = Calculate_AccessorSubtract( );
AssertAreEqual(result );
}
注意在列表中Subtract()方法在Calculate_Accessor類上而不是為Calculate類所調用因為該Subtract()方法是私有類型的所以你不能夠在Calculate類上調用它然而生成的Calculate_Accessor類卻恰到好處地暴露了該方法
如果你願意你可以使用命令行方式生成上面這個訪問器(Accessor)類Visual Studio提供了一個現成的命令行工具名字為Publicizeexe能夠幫助你針對一個類的private類型成員生成一個對應的公共類型的成員
測試私有類方法的第二種方法是使用NET反射原理借助於反射原理你可以繞過訪問限制來調用一個類的任何類型的方法和任何類的屬性列表提供的測試代理正是使用反射技術來調用私有的CalculateSubtract()方法
列表—CalculateTestcs(反射原理)
[TestMethod]
public void SubtractTest()
{
MethodInfo method = typeof(Calculate)GetMethod(Subtract
BindingFlagsNonPublic | BindingFlagsStatic);
int result = (int)methodInvoke(null new object[] { });
AssertAreEqual(result );
}
列表中的代碼通過調用一個MethodInfo對象(用於描述Subtract方法)的Invoke()方法最終調用了私有的靜態類型的Subtract()方法(我建議你把這樣的代碼打包進一個工具類中以便日後把它輕松地重用於其它測試)
七 與測試窗口有關的問題
我承認我撰寫本文的一個主要目的是我本人也為各種類型的測試窗口所疑惑不解因此我想干脆把它們整理一下歸納來看Visual Studio 共提供了三個與單元測試相關的窗口
第一個是測試結果窗口(見圖)當你運行完你的測試時將顯示這個窗口你還可以通過選擇菜單選項測試—Windows—測試結果來顯示這個窗口該測試窗口將顯示運行過的每一個測試並且顯示該測試是失敗還是順利通過測試
圖—測試結果窗口
如果你點擊標記有Test run completed(即測試運行成功)或標記有Test run failed(即測試運行失敗)的鏈接那麼呈現在你面前的將是一個關於該測試運行情況的更詳細信息的頁面
第二個窗口是測試視圖(TestView)窗口(見圖)你可以使用菜單測試—窗口—測試視圖來打開該測試視圖窗口該測試視圖窗口能夠列舉出你的所有測試你可以選擇單個的測試並運行該測試你還可以使用測試視圖中特定的測試屬性來過濾測試(例如僅僅顯示Stephen編寫的測試)
圖—測試視圖窗口
第三個與測試相關的窗口是測試列表編輯器窗口(見圖)你可以通過使用菜單測試—窗口—測試列表編輯器來打開這個窗口這個窗口能夠幫助你把你的所有測試組織成不同的列表你可以創建新的測試列表並且把相同中的測試添加到多個列表中當你需要管理上百個測試時創建多個測試列表將是非常有用的
圖—測試列表編輯器窗口
八 管理測試運行
當你執行你的單元測試超過次以上時你得到如圖所示的對話框直到我觀察到這個警告時我才認識到原來在你每次運行一個測試(每次你運行你的單元測試)時Visual Studio都會為方案中所有的程序集創建一個單獨的副本
圖—與測試運行有關的一條神秘消息
如果你使用Windows資源管理器觀察一下磁盤上你的應用程序方案文件夾那麼你會注意到Visual Studio 為你自動地創建的一個名字為TestResults的文件夾這個文件夾中針對每一個測試運行各包含相應的一個XML文件和一個子文件夾
請注意你可以通過禁用測試發布來避免Visual Studio 針對每一個測試運行創建你的程序集的副本為此你可以修改你的測試運行配置文件這僅需要選擇菜單測試—編輯測試—運行配置即可然後選擇Deployment選項卡並且在其中取消選擇Enable deployment復選框
圖—禁用測試發布功能
有時當你打開某個測試然後打開編輯測試運行配置(Test—Edit Test Run Configurations)菜單項時你會注意到出現到一條消息不存在可用的測試運行配置(No Test Run Configurations Available)這種情況下你需要在解決方案資源管理器窗口中右擊你的方案然後選擇添加—新項來添加一個新的測試運行配置當你添加一個新的測試運行配置文件之後你即可以打開圖中所示的對話框
【注意】如果你禁用測試發布功能那麼你可能無法再利用系統提供的代碼覆蓋(coverage)特征當然如果你不使用這個特征的話則不用擔心這件事情
九 從命令行運行測試
有些情況下你可能想從命令行上運行你的單元測試例如你可能不願意使用像Visual Studio這樣的集成開發環境而僅想使用記事本來書寫你所有的代碼或者更可能的是你想作為一個定制代碼登記策略的一部分來自動地運行你的測試
只要打開Visual Studio 命令提示符(程序→Microsoft Visual Studio →Visual Studio Tools→Visual Studio Command Prompt)即可以實現從命令行運行你的測試在你打開該命令提示符後導航到你的測試工程生成的程序集例如
Documents\Visual Studio \ Projects \MyMvcApp\MyMvcAppTests\Bin\ Debug
然後執行下列命令運行你的測試
mstest /testcontainer:MyMvcAppTestsdll
發出上面這個命令將啟動運行你的所有測試(見圖)
圖—從命令行運行你的單元測試
十 總結
最後再強調一下本文的目的是為了幫助你更好地理解在進行測試驅動開發時如何使用VisualStudio 編寫單元測試Visual Studio的設計支持許多不同的類型測試並且並且針對許多不同的測試用戶單單就它所提供的測試選項(以及測試相關的窗口)來說為數就相當驚人總之我希望你也同意我的觀點Visual Studio 的確是一個實現測試驅動開發的相當有效的開發環境
最後如果你忽略本本中所有其他內容的話我也建議你至少記住使用鍵盤組合鍵CtrlRA來運行你的方案中的所有測試
From:http://tw.wingwit.com/Article/program/ASP/201311/21764.html