1. 程式人生 > >C# 單元測試(入門)

C# 單元測試(入門)

注:本文示例環境

VS2017
XUnit 2.2.0 單元測試框架
xunit.runner.visualstudio 2.2.0 測試執行工具
Moq 4.7.10 模擬框架

什麼是單元測試?

確保軟體應用程式按作者的期望執行操作,其中最好的一種方法是擁有自動化測試套件。 可以對軟體應用程式進行各種不同的測試,包括整合測試、Web 測試、負載測試等。 測試各個軟體元件或方法的單元測試是最低階測試。

所謂單元測試(unit testing),就是開發者編寫的一小段程式碼,用於對軟體中的最小單元進行檢查和驗證,其一般驗證物件是一個函式或者一個類。通常而言,一個單元測試是用於判斷某個特定條件(或者場景)下某個特定函式的行為。

為什麼要使用單元測試?

  • 大大節約了測試和修改的時間,有效且便於測試各種情況。
  • 能快速定位bug(每一個測試用例都是具有針對性)。
  • 能使開發人員重新審視需求和功能的設計(難以單元測試的程式碼,就需要重新設計)。
  • 強迫開發者以呼叫者而不是實現者的角度來設計程式碼,利於程式碼之間的解耦。
  • 自動化的單元測試能保證迴歸測試的有效執行。
  • 使程式碼可以放心修改和重構。
  • 測試用例,可作為開發文件使用(測試即文件)。
  • 測試用例永久儲存,支援隨時測試。

對於我個人來說,主要是防止自己犯低階錯誤的,同時也方便修改(BUG修復)而不引入新的問題。可以放心大膽的重構。簡言之,這個簡單有效的技術就是為了令程式碼變得更加完美。

既然單元測試有這些好處,為什麼我們不去用呢?

可以歸納為以下幾個理由。

  1. 對單元測試存在的誤解,如:單元測試屬於測試工作,應該由測試人員來完成,所以單元測試不屬於開發人員的職責範圍。

    答:雖然單元測試雖然叫做"測試",但實際屬於開發範疇,應該由開發人員來做,而開發人員也能從中受益。

  2. 沒有真正意識到單元測試的收益,認為寫單元測試太費時,不值得。

    答:在開發時越早發現bug,就能節省更多的時間,降低更多的風險。單元測試先期要編寫測試用例,是需要多耗費些時間,但是後面的除錯、自測,都可以通過單元測試處理,不用手工一遍又一遍處理。實際上總時間被減少了。

  3. 專案經理或技術主管沒有要求寫單元測試,所以不用寫。

    答:寫單元測試應該成為開發人員的一種本能,開發本身就應該包含單元測試。

  4. 不知道有單元測試這回事,不知道如何用。經過這篇文件的說明,就基本知道如何處理單元測試。

框架選型

常用單元測試框架:MSTest (Visual Studio官方)、XUnit 和 NUnit。

  1. MS Test為微軟產品,整合在Visual Studio 2008+工具中。
  2. NUnit為.Net開源測試框架(採用C#開發),廣泛用於.Net平臺的單元測試和迴歸測試中,官方網址(www.nunit.org)。
  3. XUnit.Net為NUnit的改進版。

(以下主要講解MSTest 和NUnit的使用,XUnit操作和NUnit操作基本類似)

基礎實踐

開始建立你的第一個的單元測試專案吧

1)  我們先來用 VS2017 中自帶的測試模組(MSTest)來寫一個簡單的單元測試吧。
  1. 新建一個Solution,並新增專案UnitTestDemo(用於編寫被測試的專案)

  2. 在該工程中新增UnitTestClass類,並書寫一個靜態的GetTriangle(string[] sideArr) 函式用來返回一個三角形的型別。

namespace UnitTest
{
    public class UnitTestClass
    {
        /// <summary>
        /// 獲取三角形型別.
        /// </summary>
        /// <param name="sideArr">三角形三邊長度陣列.</param>
        /// <returns>返回三角形型別名稱.</returns>
        public static string GetTriangle(string[] sideArr)
        {
            string result = string.Empty;
            int a = int.Parse(sideArr[0]);
            int b = int.Parse(sideArr[1]);
            int c = int.Parse(sideArr[2]);
            if (a + b > c && a + c > b && b + c > a)
            {
                if (a == b && a == c)
                {
                    result = "等邊三角形";
                }

                if (a == b || a == c || b == c)
                {
                    result = "等腰三角形";
                }
                else
                {
                    result = "一般三角形";
                }
            }
            else
            {
                result = "不構成三角形";
            }
            return result;
        }
    }
}
  1. 然後在solution中新增一個UnitTestDemoTests測試專案,如圖所示,新增 => 新建專案之後選擇 測試 => 單元測試專案。

新建好測試專案之後,你會得到一個UnitTest1測試類模板,即一個帶有[TestClass] attribute標記的類和一個帶有[TestMethod] attribute標記的空方法public void TestMethod1()

  1. 單元測試專案無法自動訪問它正在測試的類庫。 可以通過新增對類庫專案的引用來提供測試庫訪問許可權。 為此,請右鍵單擊UnitTestProject1專案,然後依次選擇“新增” > “引用”。在“引用管理器”對話方塊中,然後選擇 UnitTestDemo專案,如下圖中所示。

    

  在UnitTestDemoTests專案中新增UnitTestDemo專案的引用,現在我們的solution就具有了下圖所示的目錄結構。

    

  5. 在UnitTestDemoTests專案中的UnitTest1類中,將模板提供的樣本單元測試程式碼替換為以下程式碼:

using UnitTest;
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTestDemoTests
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod()]
        public void GetTriangle_Test()
        {
            string[] sideArr = {"5", "5", "5"};
            Assert.AreEqual("等邊三角形", UnitTestClass.GetTriangle(sideArr));
        }
    }
}

  6. 生成UnitTestDemoTests測試專案,在生成專案後,測試項將出現在測試資源管理器中。 如果測試資源管理器視窗不可見,請選擇頂級 Visual Studio 選單上的“測試”,然後依次選擇“視窗(Windows)”、“測試資源管理器(Ctrl + E,T9)”,如圖所示。

 

  

  7. 在測試資源管理器上可以看到剛剛所寫的測試方法,這樣在GetTriangle_Test單擊右鍵選擇“執行所選定的測試”就可以在Test Explorer裡看到單元測試的執行結果,如下圖所示。  

  

可以看到,我們在單元測試中提供的例子的期望是輸出“等邊三角形”,執行結果卻是“等腰三角形”。再看一看 GetTriangle() 函式的程式碼,原來是在對在判斷三邊數值是等邊三角形之後沒有使用 else if 又用 if 判斷為等腰三角形了。通過這個簡單的單元測試就能夠發現一些意向不到的錯誤。不要以為這裡的bug很低階,類似的情況確實會在現實中發生。

 

  8. 把上面的錯誤更正後,再次執行TestMethod1()就會得到測試已通過的結果,如圖所示。

  

 

建立單元測試專案和測試方法,除了以上通過手動建立單元測試專案和根據你的要求進行編寫測試用例之外,還可以從你的專案的方法上直接生成單元測試專案和單元測試存根,那樣操作更加方便,速度也會更快一些。

2) 通過程式碼直接生成單元測試專案和單元測試存根
  1. 在程式碼編輯器視窗中,從上下文選單右鍵單擊並選擇“建立單元測試”。

  

  1. 在建立單元測試視窗,選擇預設值,或更改用於建立並命名單元測試專案和單元測試的引數值。 單擊“確定”,建立單元測試專案。

  

這裡涉及測試框架的選擇,MSTest是VS自帶的測試框架。新的MS TEST現在是通過Nuget的包釋出了,目前MS釋出了兩個版本:

  • MS TEST V1:V1的版本依賴於一個包: MSTest.TestFramework
  • MS TEST V2:V2的版本依賴於兩個包: MSTest.TestFrameworkMSTest.TestAdapter
    這兩個版本使用起來還是大同小異的,MSTest v2 主要是為了.net core準備的,當然也可以在.net framework上執行,並且在v1上新加入了一些擴充套件。
  1. 在生成的測試專案中,將測試程式碼新增到對應單元測試方法中,以使單元測試有意義。
  2. 此後生成測試專案,並在測試資源管理器中執行測試方法,得到測試結果(與上方步驟一致)。

編寫測試程式碼

你使用的單元測試框架和 Visual Studio IntelliSense 將指導你完成為程式碼專案的單元測試編寫程式碼。 若要在測試資源管理器中執行,大多數框架要求你新增特定的屬性來識別單元測試方法。 框架還提供了一種方法,通常通過斷言語句或方法屬性,來指示測試方法是否已通過或失敗。 其他屬性標識可選的安裝方法,即在類初始化時和每個測試方法和每個拆卸方法之前的安裝方法,這些拆卸方法在每個測試方法之後和類被銷燬之前執行。

AAA(準備、執行、斷言)模式是編寫待測試方法的單元測試的常用方法:

  • 準備(Arrange),單元測試方法的準備部分初始化物件並設定傳遞給待測試方法的資料;
  • 執行(Act),執行部分呼叫具有準備引數的待測試方法;
  • 斷言(Assert),斷言部分驗證待測試方法的執行行為與預期相同。
如示例中驗證 UnitTest1.GetTriangle() 函式,我們編寫了一個測試來驗證方法的標準行為:
[TestMethod()]
public void GetTriangle_Test()
{
    // arrange  
    string[] sideArr = { "5", "5", "5" }; // 準備傳給待測試方法的資料
    string expected = "等邊三角形";

    // act  
    var actual = UnitTestClass.GetTriangle(sideArr); // 呼叫測試方法

    // assert  
    Assert.AreEqual(expected, actual); // 驗證待測試方法的執行結果是否與預期相同
}
為單元測試設定超時值:

在某些情況下(例如通過網路獲取資料),常常不希望程式卡住而佔用太多時間,通過設定測試方法的超時時間,來測試一個方法是否在預期時間內執行。

[TestMethod()]
[Timeout(2000)] // 毫秒 要在單個測試方法上設定超時時間
public void GetTriangle_Test()
{   ... 
}
[TestMethod()] [Timeout(TestTimeout.Infinite)]
// 毫秒 將超時時間設定為允許的最大值 public void GetTriangle_Test() { ... }
MSTest引數化測試:

什麼是引數化測試?

答:簡單的說,就是同樣的邏輯,根據輸入引數不同給出不同的結果。因為只是引數不同,所以並不希望把測試方法寫多遍,但是又希望對每個引數的測試成為一個獨立的測試用例。舉例說,假定我有一個數學計算的方法是把兩個整數相加求和,我希望證明這個方法對於任意兩個數都是通過的。

在MSTest中可以通過DataRow Attribute 來指定測試用例的引數,實現引數化測試:

/// <summary>
/// 相加(待測試方法)
/// </summary>
/// <param name="num1">數值1</param>
/// <param name="num2">數值2</param>
/// <returns>計算結果</returns>
public static int Add(int num1, int num2)
{
    return Math.Abs(num1 + num2);
}
/// <summary>
/// 測試方法
/// </summary>
[TestMethod()]
[DataRow(10, 20)]
[DataRow(-2, -5)]
[DataRow(1, -2)]
[DataRow(5, null)]
public void Add_Test(int num1, int num2)
{
    Assert.AreEqual(UnitTestClass.Add(num1, num2), num1 + num2);
}

測試了所有可能的情況,以達到更好的覆蓋率。上方給出示例Add方法的單元測試執行測試結果如下圖所示。

  

測試結果:測試結果指出對兩個數相加操作的方法,目標方法還取了絕對值,與相應結果不符。

 

測試除錯

可以使用測試資源管理器為你的測試啟動除錯會話。 使用 Visual Studio 除錯程式可以無縫地逐句得使你在單元測試和所測試專案之間來回反覆。 若要開始除錯:

  1. 在 Visual Studio 編輯器中,在想要除錯的一個或多個測試方法中設定斷點

  2. 在測試資源管理器中,選擇測試方法,然後點選右鍵從快捷選單選擇“除錯選定的測試”。

    

  3. 進入除錯模式

F5 繼續。

F10 執行下一行程式碼,但不執行任何函式呼叫。

F11 在執行進入函式呼叫後,逐條語句執行程式碼。

Shift + F11 執行當前執行點所處函式的剩餘行。

Shift + F5 停止執行程式中的當前應用程式。可用於“中斷”模式和“執行”模式。

NUnit 測試框架

 1)下載安裝NUnit外掛

  我們在VS中選擇工具選單欄下的擴充套件和更新,選擇聯機並在搜尋框中輸入NUnit。有2個版本的Nunit介面卡,分別為NUnit 3.x(最新版為3.4.1)和NUnit 2.x(最新版為2.6.4),都支援Visual Studio 2012+。若想在VS2010中整合,需要安裝NUnit 2.6.4安裝包(可在官網下載)與VS2010 NUnit整合外掛下載,下載安裝完畢就能在 VS2010 的檢視=>其他視窗中看到 Visual Nunit (或使用快捷鍵Ctrl + F7),開啟該檢視,將之拖到合適的位置。

 2)建立NUnit單元測試專案

 未完待續..

 

使用 Visual Studio 2017進行實時單元測試

Live Unit Testing 是 Visual Studio 2017 版本 15.3 中提供的一項技術,可在我們更改程式碼,然後儲存的時候,它會自動生成自動測試,最後得出結果。

1)實時單元測試:

  • 讓你更有信心地對程式碼進行重構和更改。 Live Unit Testing 在編輯程式碼時自動執行所有受影響的測試,確保所做更改不會中斷測試。

  • 指示單元測試是否充分覆蓋程式碼,並顯示未被單元測試覆蓋的程式碼。 Live Unit Testing 以圖形方式實時描繪程式碼覆蓋率,以便一眼就能看到每行程式碼覆蓋的測試數,目和未被任何單元測試覆蓋的行。

2)進行實時單元測試:

  1. 在類庫專案中建立一個待測試方法,如下:
/// <summary>
/// 相加(待測試方法)
/// </summary>
/// <param name="num1"></param>
/// <param name="num2"></param>
/// <returns></returns>
public static int Add(int num1, int num2)
{
    return num1 + num2;
}

  2. 根據以上建立單元測試專案的過程,建立一個單元測試專案(測試框架可以使用 Live Unit Testing 的 MSTest 測試框架(預設)。 還可使用 xUnit 和 NUnit 測試框架)。在測試專案新增對被測試類庫專案的引用來提供測試庫訪問許可權。

  3. 在測試專案類中,將模板提供的樣本單元測試方法程式碼替換為以下程式碼:

[TestMethod()]
[DataRow(10, 20)]
[DataRow(-2, -5)]
[DataRow(1, -2)]
[DataRow(5, null)]
public void Add_Test(int num1, int num2)
{
    Assert.AreEqual(UnitTestClass.Add(num1, num2), num1 + num2);
}

  4. 從頂級 Visual Studio 選單中依次選擇“測試” > “Live Unit Testing” > “啟動” Visual Studio 啟動 Live Unit Testing,使其自動執行所有測試。

   

  5.  完成執行測試後,“測試資源管理器” 顯示整體結果和各個測試的結果。 此外,程式碼視窗以圖形方式顯示測試程式碼覆蓋率和測試結果。 如下圖所示,三項測試均已成功執行。 它還顯示測試中已覆蓋 Add() 方法中的所有程式碼路徑,並已成功執行這些測試(用綠色複選標記“”指示)。 UnitTestClass.cs 中的其他方法有部分程式碼沒有程式碼覆蓋率(用藍線“”指示)

  

  還可通過在程式碼視窗中選擇一個特定的程式碼覆蓋率圖示來獲得有關測試覆蓋率和測試結果的更多詳細資訊。 若要檢視此詳細資訊,請執行以下操作:

  單擊行上的綠色複選標記“”, 如下圖所示,Live Unit Testing 指示只有一個測試覆蓋該行的程式碼,並且都已成功執行。

  

  Live Unit Testing 中“”標識的主要問題是程式碼覆蓋率不完整,可以通過新增測試方法或改變測試引數,如下圖,可以看到程式碼覆蓋率已擴充套件到 GetTriangle() 的每一行程式碼。 

  在你修改原始碼時,Live Unit Testing 將自動執行新增的和修改後的測試。

  

  6. 處理測試失敗:

  將 Add() 做些許修改,修改為計算兩數相加的絕對值,在儲存後Live Unit Testing 指示 Add() 方法執行失敗,如下圖所示:  

  

7. 停止實時單元測試:

  

 

使用 IntelliTest 為你的程式碼生成單元測試

IntelliTest 瀏覽你的 .NET 程式碼,以生成測試資料和單元測試套件。 對於程式碼中的每個語句,將生成執行該語句的測試輸入。 為程式碼中的每個條件分支執行案例分析。 例如,分析 if 語句、斷言和可能引發異常的所有操作。 此分析用於為你的每個方法生成引數化單元測試的測試資料,從而建立具有較高程式碼覆蓋率的單元測試。

當你執行 IntelliTest 時,你可輕鬆看到哪些測試會失敗,並可新增任何必要的程式碼來修復它們。 你可選擇要儲存到測試專案中的已生成測試,以提供迴歸套件。 當你更改程式碼時,重新執行 IntelliTest,以使生成的測試與你的程式碼更改同步。

IntelliTest 僅可用於 C# 且不支援 x64 配置。

IntelliTest 入門

若要生成單元測試,你的型別必須是公共類。 否則,先建立單元測試,然後再生成它們。

  1. 在 Visual Studio 中開啟解決方案。 然後開啟包含你要測試的方法的類檔案。

  2. 在程式碼中右鍵單擊一種方法並選擇“建立 IntelliTest”,為方法中的程式碼建立生成單元測試專案。

  

  接受預設格式以生成測試,或更改專案和測試的命名方式。 你可以建立新的測試專案或將你的測試儲存到現有專案。

  

  3. 建立測試專案成功之後,選擇上圖中“執行 IntelliTest”,為方法中的程式碼執行IntelliTest單元測試專案。

  IntelliTest 使用不同的輸入多次執行你的程式碼。 每次執行都會在表中表示出來,顯示輸入測試資料以及產生的輸出或異常。

  

  要為一個類中的所有公共方法生成單元測試,只需右鍵單擊類而不是特定的方法。 然後選擇“執行 IntelliTest”。 使用“瀏覽結果”視窗中的下拉列表,顯示類中每個方法的單元測試和輸入資料。

  

  對於通過的測試,檢查結果列中報告的結果是否與你對程式碼的預期要求匹配。 對於失敗的測試,根據需要修復你的程式碼。 然後重新執行 IntelliTest 來驗證修復。