在開發可測試軟件的過程當中,單元測試已成爲確保軟件質量的一個不可或缺部分。測試驅動開發(Test-Driven Development,TDD)是編寫單元測試的一種方法,採用該方法的開發人員在編寫任何產品代碼以前都須要編寫測試程序。TDD 容許開發人員以系統的方式完善軟件設計,從而有效的提升單元測試的質量,增長迴歸測試(指修改代碼後的再次測試)帶來的好處。編程
當談到軟件測試時,一般是指進行的一系列不一樣種類的測試,包括單元測試、驗收測試(acceptance testing)、探索測試(exploratory testing)、性能測試(performance testing)、可擴展性測試(scalability testing)等。併發
大部分的單元測試一般具備如下 4 個特色:app
編寫單元測試時,咱們常常查找可以合理測試的最小功能片斷。在像 C# 這樣的面向對象編程語言中,類一般就意味着是最小的功能片斷,但大多數狀況下,咱們測試的是類中的一個方法。源代碼的閱讀次數遠超過編寫次數,這點在單元測試中特別有用,由於單元測試要測試軟件的指望規則和行爲。當單元測試失敗時,開發人員應該可以快速的閱讀測試程序,理解什麼出錯了,爲何會出錯,從而可以快速的修正出錯的地方。使用小的測試程序來測試小片斷代碼可以極大的改善測試結果的可理解性。 框架
單元測試另外一個重要方面是它可以在問題出現時精確的指出問題出現的位置。把測試的代碼與和它有交互的複雜代碼隔離,以確保出現的故障必定是在測試代碼中,而不是在與其交互的代碼中(檢查交互的合做代碼是否存在 bug 是合做代碼單元測試的任務)。編程語言
在修改類的內部實現時,有時,對代碼的一點點修改就可能會致使多個單元測試的失敗。所以,在修改產品代碼時,維護這些單元測試的人員一般會感到很沮喪。之因此會這樣,是由於單元測試對它要測試的類的工做原理了解太多。當編寫單元測試時,若是僅侷限於產品的公共端(一個組件的集成點),就能夠將單元測試與組件的許多內部實現細節相隔離,這樣,修改實現細節就不會常常破壞已經編寫好的單元測試了。性能
若是對每一小段代碼編寫測試程序,顯而易見,最終咱們會編寫不少單元測試。若是測試過程不是自動的,這將會損耗開發人員的大部分精力。爲了得到自動過程,開發人員一般使用單元測試框架。框架容許開發人員使用本身擅長的編程語言和開發環境編寫測試程序,而後建立 pass / fail 規則集,以後框架會斷定測試是否成功。單元測試框架中一般有一個稱爲運行程序(runner)的小軟件,可用來在項目中查找和執行單元測試。有不少這樣的軟件,一些集成到了 VS 中,一些集成到了 GUI 中,一些須要命令行運行等。單元測試
測試驅動開發指的是利用單元測試來驅動產品代碼設計的過程。首先編寫單元測試,而後編寫產品代碼使其經過測試。當把單元測試做爲質量保障機制時,主要指的是減小軟件中的漏洞。TDD 能夠實現這一目標,但這不是它的主要目標;TDD 的主要目標是提升軟件設計的質量。經過首先編寫單元測試,咱們能夠在編寫任何產品代碼以前描述想要組件執行的操做。因爲尚未產品代碼的詳細實現,所以,咱們不會把精力放到產品代碼的任何具體實現上,單元測試變成了產品代碼的消費者。測試
仍然遵循前面爲單元測試設置的指導原則:編寫小段代碼、隔離測試和自動執行測試。因爲首先編寫測試程序,所以當使用 TDD 時,常常會進入一個週期步驟:this
重複以上步驟,直到產品代碼編寫完畢爲止。因爲大部分的單元測試框架用紅色的 文本 / UI 元素表示失敗,用綠色表示經過,所以,這個週期又成爲 紅 / 綠 週期。spa
「重構」一詞具備多種意義,這裏是指在不改變產品代碼外部可見功能的狀況下,修改產品代碼實現細節的過程。在重構和更新產品代碼的過程當中,單元測試應該可以繼續經過。重構時不須要修改任何單元測試程序;若是要求必須修改單元測試,則要按照「紅 / 綠 週期」的步驟來添加、刪除或改變,切勿同時修改測試程序和產品代碼。更確切的說,重構是一種機制,在不破壞單元測試程序的狀況下,構建結構化代碼的過程。
本文中許多例子都遵守一個成爲「Arrange、Act、Assert 」的結構(3A),單元測試的代碼以下所示:
[TestMethod]
public void PoppingReturnsLastPushedItemFromStack()
{
// Arrange
Stack<string> stack = new Stack<string>();
string value = "Hello World!";
stack.Push(value);
// Act
string result = stack.Pop();
// Assert
Assert.AreEqual(value, result);
}
arrange 部分建立了一個空棧並推動一個值,這是測試功能的先決條件;act 部分從棧中彈出 arrange 部分添加的值;最後,assert 部分測試一個合乎邏輯的行爲:從棧中彈出的值和推動棧中的值是否同樣。本例中,assert 部分只有一行代碼,難道沒有許多其餘能夠斷言(編程術語,表示布爾表達式)的行爲嗎?例如,一旦從棧中彈出推動的值,棧就變空;難道咱們不該該確保它是空的嗎?若是此時再嘗試彈出另外一個值,程序就會異常;難道咱們不也應該編寫程序測試嗎?
在一個測試中,必定不要同時測試多個行爲。一個好的單元測試程序一般只測試一個很是小的功能,即一個單一行爲。本例測試的是一個非空棧彈出的已知行爲,而不是一個空棧的全部屬性,不然,應編寫更多單元測試。保持測試程序的精簡和單一意味着當修改產品代碼時只須要修改不多的地方,若是把若干個行爲混到一個單元測試(或跨過多個單元測試)中,一個單一行爲的破壞可能會致使數十個測試程序的失敗,咱們將不得不在每一個測試程序中過濾這幾個行爲以肯定出現故障的行爲。
一些開發人員將這一規則稱爲 單一斷言規則(single assertion rule)。不要誤覺得測試程序只能調用一次 Assert,其實,只要記得一次只測試一個行爲,而驗證一個合乎邏輯的行爲調用屢次 Assert 是常常的,有必要的。
儘管能夠在 VS 中直接建立單元測試項目,可是開始對 ASP.NET MVC 應用程序進行單元測試須要作大量繁瑣的工做。所以,ASP.NET MVC 團隊在 New Project 對話框中爲 ASP.NET MVC 應用程序包含了單元測試功能:
若是這樣,VS 會用一套默認的單元測試來填充新建立的項目,這些默認的單元測試能夠幫助新用戶理解如何編寫 ASP.NET MVC 應用程序的測試程序。
當建立新項目時,系統會自動打開 HomeController.cs 文件,其中包含 3 個操做方法,下面是 Index 操做方法的源代碼:
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
return View();
}
很是簡單,在 ViewBag 中設置歡迎文本併發送給視圖,而後返回一個視圖結果。在默認的單元測試項目中,Index 操做方法只有一個測試程序:
[TestMethod]
public void Index()
{
// Arrange
HomeController controller = new HomeController();
// Act
ViewResult result = controller.Index() as ViewResult;
// Assert
Assert.AreEqual("Modify this template to jump-start your ASP.NET MVC application.", result.ViewBag.Message);
}
上面是一個很是好的單元測試:按照 3A 形式編寫,3 行代碼很是容易理解。但儘管這樣,該單元測試程序仍然有待完善。雖然 Index 操做方法只有 2 行源代碼,卻要完成 3 項任務:
但這裏的默認單元測試其實是在測試這 3 個問題中的 2個,且存在一個潛在的微妙錯誤。因爲咱們讓單元測試儘量精簡、單一集中,所以這裏至少須要 2 個單元測試,一個用於測試歡迎文本,另外一個用於測試返回的視圖結果。固然,若是編寫 3 個單元測試,也不爲錯。
微妙的錯誤出如今 as 關鍵字的使用,as 的轉換若是與給定的內容不兼容,就會返回 null,而在單元測試程序的 assert 部分,在並無檢查返回的視圖結果是否爲空的狀況下,就解引用了 result。這裏將該問題標記爲待測試的第 4 個問題:操做方法不能返回 null。
as 轉換真的是必需的嗎?單元測試程序須要 ViewResult 類的一個實例才能訪問 ViewBag 屬性,這部分沒有問題。但咱們對操做方法略做修改:
public ViewResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
return View();
}
修改了返回類型後,也能夠清楚的表達代碼的功能:Index 老是返回一個視圖。這樣的一個微小修改,測試的 4 個問題減小爲 3 個問題。接下來把前面的測試程序重寫成兩個:
[TestMethod]
public void IndexShouldAskForDefaultView()
{
HomeController controller = new HomeController();
ViewResult result = controller.Index();
Assert.IsNotNull(result);
Assert.IsNull(result.ViewName);
}
[TestMethod]
public void IndexShouldSetWelcomeMessageInViewBag()
{
// Arrange
HomeController controller = new HomeController();
// Act
ViewResult result = controller.Index();
// Assert
Assert.AreEqual("Modify this template to jump-start your ASP.NET MVC application.", result.ViewBag.Message);
}
這樣修改後,看起來測試程序就好多了。as 的轉換被省略,更加詳細、描述性更強的方法名稱使咱們在不查看測試程序的內部代碼的狀況下,就能夠理解測試失敗的緣由。咱們可能不知道名爲 Index 的測試程序爲何會失敗,但咱們必定知道名爲 IndexShouldSetWelcomeMessageInViewBag 測試失敗的緣由。
新的 2 個測試方法有重複的代碼,若是進行重構,以後的代碼會以下:
private HomeController controller;
private ViewResult result;
[TestInitialize]//標識在測試以前要運行的方法,從而分配並配置測試類中的全部測試所需的資源
public void SetupContext()
{
controller = new HomeController();
result = controller.Index();
}
[TestMethod]
public void IndexShouldAskForDefaultView()
{
Assert.IsNotNull(result);
Assert.IsNull(result.ViewName);
}
[TestMethod]
public void IndexShouldSetWelcomeMessageInViewBag()
{
Assert.AreEqual("Modify this template to jump-start your ASP.NET MVC application.", result.ViewBag.Message);
}
從好的方面來講,這樣減小了代碼的重複。但很差的是,移動了測試方法中的 arrange 部分和 act 部分。若是打算以這種方式使用單元測試,那麼每一個上下文使用一個測試類最好;另外,當一個單一測試類中添加了數十(或數百)個測試時,支持全部這些測試的必要設置代碼就會變得很是多。此時,就不能清楚地知道哪一個單元測試須要哪些設置代碼了。