.NET單元測試的藝術-1.入門

開篇:最近在看Roy Osherove的《單元測試的藝術》一書,很有收穫。所以,將其記錄下來,並分爲四個部分分享成文,與各位Share。本篇做爲入門,介紹了單元測試的基礎知識,例如:如何使用一個測試框架,基本的自動化測試屬性等等,還有對應的三種測試類型。相信你能夠對編寫單元測試從一無所知到及格水平,這也是原書做者的目標。html

系列目錄:

1.入門程序員

2.核心技術數據庫

3.測試代碼網絡

1、單元測試基礎

1.1 什麼是單元測試

  一個單元測試是一段自動化的代碼,這段代碼調用被測試的工做單元,以後對這個單元的單個最終結果的某些假設進行檢驗框架

  單元測試幾乎都是用單元測試框架編寫的。單元測試容易編寫,可以快速運行。單元測試可靠、可讀,而且可維護。ide

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

1.2 與集成測試的區別

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

  總的來講,集成測試會使用真實依賴物,而單元測試則把被測試單元和其依賴物隔離開,以保證單元測試結果高度穩定,還能夠輕易控制和模擬被測試單元行爲的任何方面。                                  測試

2、測試驅動開發基礎

2.1 傳統的單元測試流程

2.2 測試驅動開發的概要流程

  如上圖所示,TDD和傳統開發方式不一樣,咱們首先會編寫一個會失敗的測試,而後建立產品代碼,並確保這個測試經過,接下來就是重構代碼或者建立另外一個會失敗的測試。this

3、第一個單元測試

3.1 NUnit 單元測試框架

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

  做爲一名.NET程序員,如何在VS中安裝NUnit並可以在VS中直接運行測試呢?

  Step1.在NuGet中找到NUnit並安裝

  Step2.在NuGet中找到NUnit Test Adapter並安裝

3.2 LogAn 項目介紹

  LogAn (Log And Notificaition)

  場景:公司有不少內部產品,用於在客戶場地監控公司的應用程序。全部這些監控產品都會寫日誌文件,日誌文件存放在一個特定的目錄中。日誌文件的格式是大家公司本身制定的,沒法用現有的第三方軟件進行解析。你的任務是:實現一個產品,對這些日誌文件進行分析,在其中搜索特定的狀況和事件,這個產品就是LogAn。找到特定的狀況和事件後,這個產品應該通知相關的人員。

  在本次的單元測試實踐中,咱們會一步一步編寫測試來驗證LogAn的解析、事件識別以及通知功能。首先,咱們須要瞭解使用NUnit來編寫單元測試。

3.3 編寫第一個測試

  (1)咱們的測試從如下這個LogAnalyzer類開始,這個類暫時只有一個方法IsValidLogFileName:

    public class LogAnalyzer
    {
        public bool IsValidLogFileName(string fileName)
        {
            if (fileName.EndsWith(".SLF"))
            {
                return false;
            }
            return true;
        }
    }

  這個方法檢查文件擴展名,據此判斷一個文件是否是有效的日誌文件。

  這裏在if中故意去掉了一個!運算符,所以這個方法就包含了一個Bug-當文件名以.SLF結尾時會返回false,而不是返回true。這樣,咱們就能看到測試失敗時在測試運行期中顯示什麼內容。

  (2)新建一個類庫項目,命名爲Manulife.LogAn.UnitTests(被測試項目項目名爲Manulife.LogAn.Lib)。添加一個類,取名爲LogAnalyzerTests.cs。

  (3)在LogAnalyzerTests類中新增一個測試方法,取名爲IsValidFileName_BadExtension_ReturnsFalse()。

  首先,咱們要明確如何編寫測試代碼,通常來講,一個單元測試一般包含三個行爲:

  所以,根據以上三個行爲,咱們能夠編寫出如下的測試方法:(其中斷言部分使用了NUnit框架提供的Assert類)

    [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]和[Test]是NUnit的特有屬性,NUnit用屬性機制來識別和加載測試。這些屬性就像一本書裏的書籤,幫助測試框架識別記載程序集裏面的重要部分,以及哪些部分是須要調用的測試。

1.[TestFixture]加載一個類上,標識這個類是一個包含自動化NUnit測試的類;

2.[Test]加在一個方法上,標識這個方法是一個須要調用的自動化測試;

  另外,再說一下測試方法名稱的規範,通常包含三個部分:[UnitOfWorkName]_[ScenarioUnderTest]_[ExpectedBehavior]

1.UnitOfWorkName  被測試的方法、一組方法或者一組類

2.Scenario  測試進行的假設條件,例如「登入失敗」,「無效用戶」或「密碼正確」等

3.ExpectedBehavior  在測試場景指定的條件下,你對被測試方法行爲的預期  

3.4 運行第一個測試

  (1)編寫好測試代碼以後,點擊"測試"->"運行"->"全部測試"

  (2)而後,點擊"測試"->"窗口"->"測試窗口管理器",你會看到如下場景

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

3.5 繼續添加測試方法

  (1)一般在進行單元測試時咱們會考慮到代碼覆蓋率,點擊"測試"->"分析代碼覆蓋率"->"全部測試",你能夠看到如下結果:80%

  (2)這時,咱們須要想出完善的測試策略來覆蓋全部的狀況,所以咱們添加一些測試方法來提升咱們的代碼覆蓋率。這裏咱們添加兩個方法,一個測試大寫文件擴展名,一個測試小寫文件擴展名:

    [Test]
    public void IsValidFileName_GoodExtensionLowercase_ReturnsTrue()
    {
        LogAnalyzer analyzer = new LogAnalyzer();
        bool result = analyzer.IsValidLogFileName("filewithgoodextension.slf");
        Assert.AreEqual(true, result);
    }

    [Test]
    public void IsValidFileName_GoodExtensionUppercase_ReturnsTrue()
    {
        LogAnalyzer analyzer = new LogAnalyzer();
        bool result = analyzer.IsValidLogFileName("filewithgoodextension.SLF");
        Assert.AreEqual(true, result);
    }

  這時測試結果以下圖所示:

  這時再來看看代碼覆蓋率:100%

  (3)爲了讓全部的測試都能經過,這時咱們須要修改源代碼,改用大小寫不敏感的字符串匹配:

    public bool IsValidLogFileName(string fileName)
    {
        if (!fileName.EndsWith(".SLF", StringComparison.CurrentCultureIgnoreCase))
        {
            return false;
        }
        return true;
    }

  這時,咱們再來運行一下全部的測試(也能夠選擇 運行未經過的測試)來看下由紅到綠的快感。單元測試的理念很簡單:只有全部的測試都經過,繼續前行的綠燈纔會亮起。哪怕只有一個測試失敗了,進度條上都會亮起紅燈,顯示你的系統(或者測試)出現了問題。

4、更多的NUnit

4.1 參數化重構單元測試

  NUnit中有個叫作 參數化測試(Parameterized Tests)的功能,咱們能夠藉助[TestCase]標籤特性來重構咱們的單元測試:

    [TestCase("filewithgoodextension.slf")]
    [TestCase("filewithgoodextension.SLF")]
    public void IsValidFileName_ValidExtensions_ReturnsTrue(string fileName)
    {
        LogAnalyzer analyzer = new LogAnalyzer();
        bool result = analyzer.IsValidLogFileName(fileName);
        Assert.AreEqual(true, result);
    }

  能夠看到,藉助TestCase特性,測試數目沒有改變,可是測試代碼卻變得更易維護,更加易讀。

4.2 SetUp和TearDown

  NUnit還有一些特別的標籤特性,能夠很方便地控制測試先後的設置和清理狀態工做,他們就是[SetUp]和[TearDown]。

1.[SetUp] 這個標籤加在一個方法上,NUnit每次在運行測試類裏的任何一個測試時都會先運行這個setup方法;

2.[TearDown] 這個標籤標識一個方法應該在測試類裏的每一個測試運行完成以後執行;

    [TestFixture]
    public class LogAnalyzerTests
    {
        private LogAnalyzer analyzer = null;
        [SetUp]
        public void Setup()
        {
            analyzer = new LogAnalyzer();
        }

        [Test]
        public void IsValidFileName_ValidFileLowerCased_ReturnsTrue()
        {
            bool result = analyzer.IsValidLogFileName("whatever.slf");
            Assert.IsTrue(result, "filename should be valid!");
        }

        [Test]
        public void IsValidFileName_ValidFileUpperCased_ReturnsTrue()
        {
            bool result = analyzer.IsValidLogFileName("whatever.SLF");
            Assert.IsTrue(result, "filename should be valid!");
        }

        [TearDown]
        public void TearDown()
        {
            analyzer = null;
        }
    }

  咱們能夠把setup和teardown方法想象成測試類中測試的構造函數和析構函數,在每一個測試類中只能有一個setup和teardown方法,這兩個方法對測試類中的每一個方法只執行一次。

  不過,使用[Setup]越多,測試代碼可讀性就越差。原書做者推薦採用工廠方法(Factory Method)初始化被測試的實例。

    /// <summary>
    /// 工廠方法初始化 LogAnalyzer 
    /// 既節省編寫代碼的時間,又使每一個測試內的代碼更簡潔易讀
    /// 同時保證 LogAnalyzer 老是用一樣的方式初始化
    /// </summary>
    private static LogAnalyzer MakeAnalyzer()
    {
        return new LogAnalyzer();
    }

  在測試方法中能夠直接使用:

    [Test]
    public void IsValidFileName_BadExtension_ReturnsFalse()
    {
        LogAnalyzer analyzer = MakeAnalyzer();
        bool result = analyzer.IsValidLogFileName("filewithbadextension.foo");
        Assert.AreEqual(false, result);
    }

4.3 檢驗預期的異常

  不少時候,咱們的方法中會拋出一些異常,這時若是咱們的測試也應該作一些修改。在NUnit中,提供了一個API : Assert.Catch<T>(delegate)

  首先,咱們修改一下被測試的方法,增長一行判斷文件名是否爲空的代碼:

    public bool IsValidLogFileName(string fileName)
    {
        if(string.IsNullOrEmpty(fileName))
        {
            throw new ArgumentException("filename has to be provided");
        }

        if (!fileName.EndsWith(".SLF", StringComparison.CurrentCultureIgnoreCase))
        {
            return false;
        }
        return true;
    }

  而後,咱們新增一個測試方法,使用Assert.Catch來檢測異常是否一致:

    [Test]
    public void IsValidFileName_EmptyName_Throws()
    {
        LogAnalyzer analyzer = new LogAnalyzer();
        // 使用Assert.Catch
        var ex = Assert.Catch<Exception>(() => analyzer.IsValidLogFileName(string.Empty));
        // 使用Assert.Catch返回的Exception對象
        StringAssert.Contains("filename has to be provided", ex.Message);
    }

4.4 忽略測試

  有時候測試代碼有問題,可是咱們又須要把代碼簽入到主代碼樹中。在這種罕見的狀況下(雖然確實很是少),能夠給那些測試代碼自身有問題的測試加一個[Ignore]標籤特性。

    [Test]
    [Ignore("there is a problem with this test!")]
    public void IsValidFileName_ValidFile_ReturnsTrue()
    {
        // ...
    }

  能夠看到,這個測試確實被忽略了:

4.5 設置測試的類別

  咱們能夠把測試按照指定的測試類別運行,使用[Category]標籤特性就能夠實現這個功能:

    [Test]
    [Category("Fast Tests")]
    public void IsValidFileName_BadExtension_ReturnsFalse()
    {
        LogAnalyzer analyzer = new LogAnalyzer();
        bool result = analyzer.IsValidLogFileName("filewithbadextension.foo");
        Assert.AreEqual(false, result);
    }

4.6 測試系統狀態的改變

  此前咱們得測試都有返回值,而不少要測試的方法都沒有返回值,而只是改變對象中的某些狀態,咱們又該如何測試呢?

  首先,咱們修改IsValidLogFileName方法,增長一個狀態屬性:

    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", StringComparison.CurrentCultureIgnoreCase))
            {
                return false;
            }

            // 改變系統狀態
            WasLastFileNameValid = true;

            return true;
        }
    }

  其次,咱們編寫一個測試,對系統狀態進行斷言:

    [TestCase("badfile.foo", false)]
    [TestCase("goodfile.slf", true)]
    public void IsValidFileName_WhenCalled_ChangesWasLastFileNameValid(string fileName, bool expected)
    {
        LogAnalyzer analyzer = new LogAnalyzer();
        analyzer.IsValidLogFileName(fileName);
        Assert.AreEqual(expected, analyzer.WasLastFileNameValid);
    }

5、小結

  這一篇做爲入門,帶領你們領略了一下單元測試的概念,如何編寫單元測試,如何在VS中應用NUnit進行單元測試。相信你們之前都用過MSTest,而咱們這裏卻使用了NUnit。因此,下面咱們來總結一下MSTest與NUnit在特性標籤上的一些區別:

MS Test Attribute NUnit Attribute 用途
[TestClass] [TestFixture] 定義一個測試類,裏面能夠包含不少測試函數和初始化、銷燬函數(如下全部標籤和其餘斷言)。
[TestMethod] [Test] 定義一個獨立的測試函數。
[ClassInitialize] [TestFixtureSetUp] 定義一個測試類初始化函數,每當運行測試類中的一個或多個測試函數時,這個函數將會在測試函數被調用前被調用一次(在第一個測試函數運行前會被調用)。
[ClassCleanup] [TestFixtureTearDown] 定義一個測試類銷燬函數,每當測試類中的選中的測試函數所有運行結束後運行(在最後一個測試函數運行結束後運行)。
[TestInitialize] [SetUp] 定義測試函數初始化函數,每一個測試函數運行前都會被調用一次。
[TestCleanup] [TearDown] 定義測試函數銷燬函數,每一個測試函數執行完後都會被調用一次。
[AssemblyInitialize] -- 定義測試Assembly初始化函數,每當這個Assembly中的有測試函數被運行前,會被調用一次(在Assembly中第一個測試函數運行前會被調用)。
[AssemblyCleanup] -- 定義測試Assembly銷燬函數,當Assembly中全部測試函數運行結束後,運行一次。(在Assembly中全部測試函數運行結束後被調用)
[DescriptionAttribute] [Category] 定義標識分組。

   目前爲止,咱們的單元測試都還很簡單也還比較順利。可是,若是咱們要測試的方法依賴於一個外部資源,如文件系統、數據庫、Web服務或者其餘難以控制的東西,那又該如何編寫測試呢?爲了解決這些問題,咱們須要建立測試存根僞對象模擬對象,下一篇核心技術將會介紹這些內容,讓咱們跟隨Roy Osherove的《單元測試的藝術》一塊兒去探尋吧。

參考資料 

      The Art of Unit Testing

  (1)Roy Osherove 著,金迎 譯,《單元測試的藝術(第2版)》

  (2)Aileer,《對比MS Test與NUnit Test框架

 

相關文章
相關標籤/搜索