前記:前段時間團隊在推行單元測試,對於分配的測試任務也很快的完成,但以爲本身對單元測試的理解也不夠透徹,因此就買了《單元測試的藝術》這本書來尋找一些我想要的答案。這本書並非手把手教你寫單元測試代碼的,而是教你一些思想,按部就班,最終達到可以寫出可靠的、可維護的、可讀的測試。本篇文章是入門篇,主要是講解單元測試的概念、與集成測試的區別以及如何使用框架進行最基礎的單元測試等。數據庫
單元測試是一段自動化的代碼,這段代碼調用被測試的工做單元,以後對這個單元的單個最終結果的某些假設進行檢驗。單元測試幾乎都是用單元測試框架編寫的。單元測試容易編寫,能快速運行。單元測試可靠、可讀、而且可維護。只要產品代碼不發生變化,單元測試的結果是穩定的。網絡
特徵:架構
集成測試是對一個工做單元進行的測試,這個測試對被測試的工做單元沒有徹底的控制,並使用該單元的一個或多個真實依賴物,例如時間,網絡、數據庫、線程或隨機數產生器等。框架
單元測試與集成測試最大的區別在於:集成測試依賴於一個或多個真實的模塊,當運行集成測試時,出現失敗的狀況後你並不能當即判斷是哪裏出了問題,所以找到缺陷的根源會比較困難。ide
[虛線表明是一個可選的行爲]函數
[這是一個螺旋式的過程]單元測試
由上面的兩個圖中能夠看出TDD與傳統開發模式的區別:先編寫一個會失敗的測試,而後建立產品代碼,並確保這個測試經過,接下來是重構代碼或者建立另外一個會失敗的測試。測試
單元測試框架是幫助開發人員進行單元測試的代碼庫和模塊。編碼
NUnit 是一套開源的基於.NET平臺的類Xunit白盒測試架構,支持全部的.NET平臺。這套架構的特色是開源,使用方便,功能齊全。很適合做爲.NET語言開發的產品模塊的白盒測試框架。spa
起初是從流行的Java單元測試框架JUnit直接移植過來的,以後NUnit在設計和可用性上作了極大地改進,和JUnit有了很大的區別,給突飛猛進的測試框架生態系統注入了新的活力。
如何在VS安裝並運行呢?用Nuget是最方便的一種形式了,以下圖:
(1)假定咱們要測試下面這段代碼:
public class LogAnalyzer { public bool IsValidLogFileName(string fileName) { if (fileName.EndsWith(".SLF")) { return false; } return true; } }
這個方法是用來檢查文件擴展名的,以此判斷是不是一個有效的文件。在上面的程序中,故意在if條件語句中少了一個‘!’號,這樣,咱們能夠看到測試失敗時在測試運行期間會顯示什麼內容。
(2)新建一個類庫項目,名稱最好爲[ProjectUnderTest].UnitTests;並添加一個類,類型爲[ClassName]Tests的類;在類中就能夠寫測試方法,通常測試方法是這樣子來命名的:[UnitOfWorkName]_[ScenarioUnderTest]_[ExceptedBehavior]。
(3)咱們須要明確的是如何編寫測試代碼,通常來講,一個單元測試包含三個行爲:
① 準備(Arrange)對象,建立對象,進行必要的設置;
② 操做(Act)對象;
③ 斷言(Assert)某件事情是預期的;
(4)根據以上步驟,編寫第一個單元測試方法
[TestFixture] public class LogAnalyzerTests { [Test] public void IsValidFileName_BadExtension_ReturnsFalse() { LogAnalyzer analyzer = new LogAnalyzer(); bool result = analyzer.IsValidLogFileName("filewithbadextension.foo"); Assert.AreEqual(false, result); } }
其中,屬性[TestFixture]:標識這個類是一個包含自動化NUnit測試的類和[Test]:標識這個方法是一個須要調用的自動化測試是NUnit的特有屬性,NUnit用屬性機制來識別和加載測試。
從上圖能夠看出,測試方法並無經過,咱們指望(Expected)的結果是False,而實際(Actual)的結果倒是True。而且還幫你指出了行號。
NUnit有個很酷的功能,叫作參數化測試。能夠從現有的測試中任意選擇一個,進行一下修改:
(1)把屬性[Test]替換成屬性[TestCase]
(2)把測試中用到的硬編碼的值替換成這個測試方法的參數
(3)把替換掉的值放在屬性的括號中[TestCase(param1,param2,...)]
[TestCase("filewithbadextension.SLF")] [TestCase("filewithbadextension.slf")] public void IsValidLogFileName_ValidExtensions_ReturnsTrue(string file) { LogAnalyzer analyzer=new LogAnalyzer(); bool result = analyzer.IsValidLogFileName(file); Assert.True(result); }
須要注意的是:這個時候你須要用一個比較通用的名字從新命令這個測試方法。
固然,[TestCase("")]不只僅只能夠寫一個參數,也能夠寫N個參數。
[TestCase("filewithbadextension.SLF",true)] [TestCase("filewithbadextension.slf",true)] public void IsValidLogFileName_ValidExtensions_ReturnsTrue(string file,bool excepted) { LogAnalyzer analyzer = new LogAnalyzer(); bool result = analyzer.IsValidLogFileName(file); Assert.AreEqual(excepted,result); }
進行單元測試時,很重要的一點是保證以前測試的遺留數據或者實例獲得銷燬,新測試的狀態是重建的。幸虧,NUnit有一些特別的屬性,能夠很方便地控制測試先後的設置和清理狀態工做,就是[SetUp]和[TearDown]動做屬性。
[SetUp] NUnit每次在運行測試類裏的任何一個測試時都會先運行這個方法
[TearDown] 這個屬性標識一個方法應該在測試類裏的每一個測試運行以後執行。
private LogAnalyzer _logAnalyzer = null; [SetUp] public void Setup() { _logAnalyzer=new LogAnalyzer(); } [Test] public void IsValidFileName_validFileLowerCased_ReturnsTrue() { bool result = _logAnalyzer.IsValidLogFileName("hello.slf"); Assert.IsTrue(result,"filename should be valid!"); } [Test] public void IsValidFileName_validFileUpperCased_ReturnsTrue() { bool result = _logAnalyzer.IsValidLogFileName("hello.SLF"); Assert.IsTrue(result, "filename should be valid!"); } [TearDown] public void TearDown() { _logAnalyzer = null; }
雖然SetUp與TearDown用起來很方便,可是不建議使用,由於這種方式隨着代碼的增長,後面測試方法很快就變得難以閱讀了,最好是採用工廠方法來初始化被測試的實例。
咱們如今修改一下要測試的代碼,在輸入爲Null或者Empty的時候,就跑出一個異常。
public class LogAnalyzer { public bool IsValidLogFileName(string fileName) {
if(string.IsNullOrEmpty(fileName))
{
throw new ArgumentException("filename has to be provided");
}
if (fileName.EndsWith(".SLF")) { return false; } return true; }
測試代碼以下:
[Test] [ExpectedException(typeof (ArgumentException), ExceptedMessage = "fileName has to be provided")] public void IsValidFileName_EmptyFileName_ThrowsException() { MakeLogAnalyzer().IsValidLogFileName(string.Empty); } private LogAnalyzer MakeLogAnalyzer() { return new LogAnalyzer(); }
注意:以上的代碼雖然是正確的,可是在NUint3.0中已經棄用了,緣由是採用這種方法,你可能不知道哪一行代碼拋出的這個異常,若是你的構造函數有問題,也拋出這個異常,那你所寫的測試也會經過,但事實上是錯誤的。NUint提供了一個新的API,Assert.Catch<T>(delegate)。如下是使用Assert.Catch編寫的測試代碼:
[Test] public void IsValidFileName_EmptyFileName_ThrowsException() { var ex = Assert.Catch<ArgumentException>(() => { MakeLogAnalyzer().IsValidLogFileName(""); }); StringAssert.Contains("fileName has to be provided",ex.Message); } private LogAnalyzer MakeLogAnalyzer() { return new LogAnalyzer(); }
有時候代碼有問題,可是你又須要把代碼簽入到主代碼中(這種狀況應該是少中極少,由於這是一種錯誤的方式)。能夠採用[Ignore]屬性。示例以下:
[Test] [Ignore("it has some problems")] public void IsValidFileName_validFileUpperCased_ReturnsTrue() { bool result = MakeLogAnalyzer().IsValidLogFileName("hello.SLF"); Assert.IsTrue(result, "filename should be valid!"); }
結果以下:
能夠把測試按指定的測試類別運行,例如:慢測試和快測試。使用[Category]屬性能夠實現這個功能。
[Test] [Category("Fast Tests")] public void IsValidFileName_ValidFile_ReturnTrue() { Assert.IsTrue(MakeLogAnalyzer().IsValidLogFileName("xxx.SLF")); }
上面全部測試示例,都是有根據被測試方法的返回值來進行測試,但一個工程裏面不可能每一個方法都是有返回值的,有的是須要判斷系統狀態的改變的,稱爲基於狀態的測試。
定義:經過檢查被測試系統及其協做方(依賴物)在被測試方法執行後行爲的改變,斷定被測試方法是否正確工做。
//被測試代碼
public class LogAnalyzer { public bool WasLastFileNameValid { get; set; } public bool IsValidLogFileName(string fileName) { WasLastFileNameValid = false; if (string.IsNullOrEmpty(fileName)) { throw new ArgumentException("fileName has to be provided"); } if (!fileName.EndsWith(".SLF")) { return false; } WasLastFileNameValid = true; return true; } }
測試代碼:
[TestCase("filewithbadextension.SLF", true)] [TestCase("filewithbadextension.slf", true)] public void IsValidLogFileName_ValidExtensions_ReturnsTrue(string file, bool excepted) { LogAnalyzer analyzer = MakeLogAnalyzer(); analyzer.IsValidLogFileName(file); Assert.AreEqual(excepted, analyzer.WasLastFileNameValid); } private LogAnalyzer MakeLogAnalyzer() { return new LogAnalyzer(); }