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

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

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

系列目錄:

1.入門

2.核心技術

3.測試程式碼

一、單元測試基礎

1.1 什麼是單元測試

  一個單元測試是一段自動化的程式碼,這段程式碼呼叫被測試的工作單元,之後對這個單元的單個最終結果的某些假設進行檢驗

  單元測試幾乎都是用單元測試框架編寫的。單元測試容易編寫,能夠快速執行。單元測試可靠、可讀,並且可維護。

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

1.2 與整合測試的區別

整合測試是對一個工作單元進行的測試,這個測試對被測試的工作單元沒有完全的控制,並使用該單元的一個或多個真實依賴物,例如時間、網路、資料庫、執行緒或隨機數產生器等。

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

二、測試驅動開發基礎

2.1 傳統的單元測試流程

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

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

三、第一個單元測試

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

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

四、更多的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);
    }

五、小結

  這一篇作為入門,帶領大家領略了一下單元測試的概念,如何編寫單元測試,如何在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版)》

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

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

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

.NET單元測試藝術-3.測試程式碼

開篇:上一篇我們學習單元測試和核心技術:存根、模擬物件和隔離框架,它們是我們進行高質量單元測試的技術基礎。本篇會集中在管理和組織單元測試的技術,以及如何確保在真實專案中進行高質量的單元測試。 系列目錄: 1.入門 2.核心技術 3.測試程式碼 一、測試層次和組織 1.1 測試專案的兩種目錄

.NET單元測試藝術-2.核心技術

開篇:上一篇我們學習基本的單元測試基礎知識和入門例項。但是,如果我們要測試的方法依賴於一個外部資源,如檔案系統、資料庫、Web服務或者其他難以控制的東西,那又該如何編寫測試呢?為了解決這些問題,我們需要建立測試存根、偽物件及模擬物件。這一篇中我們會開始接觸這些核心技術,藉助存根破除依賴,使用模擬物件進行互動測

Juint 單元測試1

cti path 語言 html size add 版本號 icon build Junit 是一個基於Java語言的回歸單元測試框架。是白盒測試的一種技術,記住這些就可以了。 為項目添加Junit 1 右鍵項目名稱選擇“Properties”,在彈

.Net單元測試業務實踐

elm com cursor sco toa void 重點 aci data code[class*="langua

【C#】【xUnit】【Moq】.NET單元測試Mock框架Moq初探!

在TDD開發模型中,經常是在編碼的同時進行單元測試的編寫,由於現代軟體開發不可能是一個人完成的工作,所以在定義好介面的時候我們就可以進行自己功能的開發(介面不能經常變更),而我們呼叫他人的功能時只需要使用介面即可。 但我們在編寫自己的單元測試並進行功能驗證的時候,如果介面的實現人還沒有完成程式碼怎麼

.net 單元測試工具:自帶 mstest

轉載地址: http://blog.csdn.net/metal1/article/details/9630451    在我的專案中使用的VS自帶測試工具MSTEST而不是NUNIT,   有時需要自動化測試VS中的程式碼,便於整合,需要在命令列執行。  

JustMock .NET單元測試利器(二)JustMock基礎

Mock是Telerik®JustMock框架中的主要類。Mock用於建立例項和靜態模擬,安排和驗證行為。 本文將介紹 “Mock”的基本用法: 首先我們建立一個IFoo物件 publicinter

JustMock .NET單元測試利器(一)

1.什麼是Mock? Mock一詞是指模仿或者效仿,用於建立例項和靜態模擬、安排和驗證行為。在軟體開發中提及"mock",通常理解為模擬物件。模擬物件的概念就是我們想要建立一個可以替代實際物件的物件,

.Net單元測試之NMock

下面是一個使用NMock的快速教程: 示例 現在我們來做一個簡單的“Hello”例子,測試 Hello 類的 Greet() 方法,Hello 類依賴於一個 Person 物件,並將根據 Person 的名字向對應的賬號傳送祝賀(Greet)。這個例子很簡單,其實用不到模擬物件,不過用來理解 NMock 是很

ML.NET技術研究系列1-入門

近期團隊在研究機器學習,希望通過機器學習實現補丁釋出評估,系統異常檢測。業務場景歸納一下: 收集整理資料(釋出相關的異常日誌、告警資料),標識出補丁釋出情況(成功、失敗) 選擇一個機器學習的Model進行Train訓練 基於訓練出的模型(準確度要高)進行最新補丁釋出情況預測 典型的機器學習-監

單元測試藝術-入門

驅動開發 ava clas als 概念 內容 其他 並不是 int 前記:前段時間團隊在推行單元測試,對於分配的測試任務也很快的完成,但覺得自己對單元測試的理解也不夠透徹,所以就買了《單元測試的藝術》這本書來尋找一些我想要的答案。這本書並不是手把手教你寫單元測試代碼的,而

使用xUnit為.net core程式進行單元測試(1)

導讀 為什麼要編寫自動化測試程式(Automated Tests)? 可以頻繁的進行測試 可以在任何時間進行測試,也可以按計劃定時進行,例如:可以在半夜進行自動測試。 肯定比人工測試要快。 可以更快速的發現錯誤。 基本上是非常可靠的。 測試程式碼與生產程式碼緊密結合。 使得開發團隊更具有幸

.NET Core之單元測試(一):入門

目錄 什麼是單元測試 .NET Core中的測試框架 一個最基礎的單元測試 我們再看看上面的程式碼 什麼是單元測試 單元測試是對軟體中的最小可測試單元進行檢查和驗證。對於單元測試

ASP.NET Zero--單元測試

正在 模型 ldl git target xuni false pre misc 單元測試 ASP.NET Zero啟動項目包含單元和集成測試。使用以下工具開發測試: xUnit作為測試框架。 Shouldly 作為斷言庫。 Microsoft.EntityFr

up7.1-asp.net-本地測試教程

baidu height 數據表 ng- padding mil sql .config -h 1.1. ASP.NET 框架:.NET Framework 4.5 依賴庫:csredis,Newtonsoft.Json   安裝redis 下載 re

單元測試藝術 ---- 系列文章

sts 系列文章 logs cnblogs cat href htm 藝術 tle .NET單元測試的藝術-1.入門 .NET單元測試的藝術-2.核心技術 .NET單元測試的藝術-3.測試代碼 出處:http://www.cnblogs.com/edisonchou/

《Java從入門到放棄》JavaSE入門篇:單元測試

java 單元測試 單元測試其實沒什麽好說的,直接看操作步驟!我們來測試前一篇的小明買食物的方法。第一步:在小明類上點右鍵,然後再new一個JUnit Test Case第二步:繼續點下一步,圖上的內容相信大家都看得懂吧,如果看不懂···,那就要麽學習,要麽放棄吧,哈哈!第三步:勾選要測試的方法:第四

輕松掌握VS Code開發.Net Core及創建Xunit單元測試

blog logs 寫文章 編譯 分享 單獨 etc 2.0 ren 前言 本篇文章主要還是介紹使用 VS Code 進行.Net Core開發和常用 CLI命令的使用,至於為啥要用VS Code ,因為它是真的是好看又好用 :) ,哈哈,主要還是為了跨平臺開發做準備。 開

VS2015 c++程序單元測試初探——從0到1中所遇到的錯誤和歷程

ima 資料 因此 開頭 完成 數字 style debug unit 實現過程 一開始對單元測試這個東西感覺很恐懼,在看過雪晴的博客後,覺得自己可以試試學學,找到了一篇博客,地址: VS2015安裝與C++進行簡單單元測試 前面的建立和初始化都比較easy,但很快遇到了一