單元測試的藝術-入門篇

前記:前段時間團隊在推行單元測試,對於分配的測試任務也很快的完成,但以爲本身對單元測試的理解也不夠透徹,因此就買了《單元測試的藝術》這本書來尋找一些我想要的答案。這本書並非手把手教你寫單元測試代碼的,而是教你一些思想,按部就班,最終達到可以寫出可靠的、可維護的、可讀的測試。本篇文章是入門篇,主要是講解單元測試的概念、與集成測試的區別以及如何使用框架進行最基礎的單元測試等。數據庫

1、單元測試的基礎

1.一、什麼是單元測試

  單元測試是一段自動化的代碼,這段代碼調用被測試的工做單元,以後對這個單元的單個最終結果的某些假設進行檢驗。單元測試幾乎都是用單元測試框架編寫的。單元測試容易編寫,能快速運行。單元測試可靠、可讀、而且可維護。只要產品代碼不發生變化,單元測試的結果是穩定的網絡

  特徵:架構

  • 自動化、可重複執行;
  • 很容易實現;
  • 次日還有意義;
  • 任何人都應該能一鍵運行它;
  • 運行速度應該很快;
  • 結果應該是穩定的;
  • 能徹底控制被測試的單元;
  • 徹底隔離(獨立於其餘測試的運行);

1.二、什麼是集成測試

  集成測試是對一個工做單元進行的測試,這個測試對被測試的工做單元沒有徹底的控制,並使用該單元的一個或多個真實依賴物,例如時間,網絡、數據庫、線程或隨機數產生器等。框架

1.三、單元測試與集成測試的區別在哪裏?

  單元測試與集成測試最大的區別在於:集成測試依賴於一個或多個真實的模塊,當運行集成測試時,出現失敗的狀況後你並不能當即判斷是哪裏出了問題,所以找到缺陷的根源會比較困難ide

  

 

2、TDD(測試驅動開發)

2.一、傳統的開發流程

       [虛線表明是一個可選的行爲]函數

  

 

 

2.二、TDD的開發流程

  [這是一個螺旋式的過程]單元測試

  

由上面的兩個圖中能夠看出TDD與傳統開發模式的區別:先編寫一個會失敗的測試,而後建立產品代碼,並確保這個測試經過,接下來是重構代碼或者建立另外一個會失敗的測試。測試

3、開始使用框架進行基礎的單元測試

3.一、單元測試框架的做用

  單元測試框架是幫助開發人員進行單元測試的代碼庫和模塊。編碼

3.二、NUnit

  NUnit 是一套開源的基於.NET平臺的類Xunit白盒測試架構,支持全部的.NET平臺。這套架構的特色是開源,使用方便,功能齊全。很適合做爲.NET語言開發的產品模塊的白盒測試框架。spa

     起初是從流行的Java單元測試框架JUnit直接移植過來的,以後NUnit在設計和可用性上作了極大地改進,和JUnit有了很大的區別,給突飛猛進的測試框架生態系統注入了新的活力。

     如何在VS安裝並運行呢?用Nuget是最方便的一種形式了,以下圖:

 

3.三、編寫第一個單元測試

  (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用屬性機制來識別和加載測試。

3.四、運行過程與結果

  

  

  從上圖能夠看出,測試方法並無經過,咱們指望(Expected)的結果是False,而實際(Actual)的結果倒是True。而且還幫你指出了行號。

4、更多NUnit屬性的介紹

4.一、參數化測試

  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);
        }

4.二、[Setup]與[TearDown]

  進行單元測試時,很重要的一點是保證以前測試的遺留數據或者實例獲得銷燬,新測試的狀態是重建的。幸虧,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用起來很方便,可是不建議使用,由於這種方式隨着代碼的增長,後面測試方法很快就變得難以閱讀了,最好是採用工廠方法來初始化被測試的實例。

4.三、檢驗預期的異常

  咱們如今修改一下要測試的代碼,在輸入爲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();
        }

4.四、忽略測試

  有時候代碼有問題,可是你又須要把代碼簽入到主代碼中(這種狀況應該是少中極少,由於這是一種錯誤的方式)。能夠採用[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!");
        }

  結果以下:

4.五、設置測試的類型

  能夠把測試按指定的測試類別運行,例如:慢測試和快測試。使用[Category]屬性能夠實現這個功能。

        [Test]
        [Category("Fast Tests")]
        public void IsValidFileName_ValidFile_ReturnTrue()
        {
            Assert.IsTrue(MakeLogAnalyzer().IsValidLogFileName("xxx.SLF"));
        }

4.六、測試系統狀態的改變而非返回值

  上面全部測試示例,都是有根據被測試方法的返回值來進行測試,但一個工程裏面不可能每一個方法都是有返回值的,有的是須要判斷系統狀態的改變的,稱爲基於狀態的測試。

  定義:經過檢查被測試系統及其協做方(依賴物)在被測試方法執行後行爲的改變,斷定被測試方法是否正確工做。

    //被測試代碼
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();
        }

5、總結

  • 建立測試類、項目和方法的管理;
  • 測試命名要有規範;
  • 使用工廠方法重用測試中的代碼,例如用來建立和初始化全部測試都要用到的對象代碼;
  • 儘可能不要使用[SetUp]和[TearDown],由於它們使測試變得難以理解。
相關文章
相關標籤/搜索