1. 程式人生 > >單元測試的藝術-入門篇

單元測試的藝術-入門篇

驅動開發 ava clas als 概念 內容 其他 並不是 int

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

一、單元測試的基礎

1.1、什麽是單元測試

  單元測試是一段自動化的代碼,這段代碼調用被測試的工作單元,之後對這個單元的單個最終結果的某些假設進行檢驗。單元測試幾乎都是用單元測試框架編寫的。單元測試容易編寫,能快速運行。單元測試可靠、可讀、並且可維護

。只要產品代碼不發生變化,單元測試的結果是穩定的

  特征:

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

1.2、什麽是集成測試

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

1.3、單元測試與集成測試的區別在哪裏?

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

  技術分享

二、TDD(測試驅動開發)

2.1、傳統的開發流程

[虛線代表是一個可選的行為]

  技術分享

2.2、TDD的開發流程

  [這是一個螺旋式的過程]

  技術分享

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

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

3.1、單元測試框架的作用

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

3.2、NUnit

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

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

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

 技術分享

3.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.4、運行過程與結果

  技術分享

  技術分享

  從上圖可以看出,測試方法並沒有通過,我們期望(Expected)的結果是False,而實際(Actual)的結果卻是True。並且還幫你指出了行號。

四、更多NUnit屬性的介紹

4.1、參數化測試

  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.2、[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.3、檢驗預期的異常

  我們現在修改一下要測試的代碼,在輸入為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.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.5、設置測試的類型

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

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

4.6、測試系統狀態的改變而非返回值

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

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

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

五、總結

  • 創建測試類、項目和方法的管理;
  • 測試命名要有規範;
  • 使用工廠方法重用測試中的代碼,例如用來創建和初始化所有測試都要用到的對象代碼;
  • 盡量不要使用[SetUp]和[TearDown],因為它們使測試變得難以理解。

單元測試的藝術-入門篇