1. 程式人生 > >關於C#程式的單元測試

關於C#程式的單元測試

目錄

  • 1.單元測試概念
  • 2.單元測試的原則
  • 3.單元測試簡單示例
  • 4.單元測試框架特性標籤
  • 5.單元測試中的斷言Assert
  • 6.單元測試中驗證預期的異常
  • 7.單元測試中針對狀態的間接測試
  • 8.單元測試在MVC模式中的實現
  • 8.單元測試相關參考
  • 9.示例原始碼下載
志銘-2020年1月23日 11:49:41

1.單元測試概念

  • 什麼是單元測試?

    單元測試(unit testing)是一段自動化的程式碼,用來呼叫被測試的方法或類,而後驗證基於該方法或類的邏輯行為的一些假設。

    簡而言之說:單元測試是一段程式碼(通常一個方法)呼叫另外一段程式碼,隨後檢驗一些假設的正確性。

    在過程化程式設計中,一個單元就是單個程式、函式、過程等;

    對於面向物件程式設計,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。

  • 為什麼要單元測試?

    單元測試的目標是隔離程式部件並證明這些單個部件是正確的。單元測試在軟體開發過程的早期就能發現問題。

    在程式碼重構或是修改的時候,可以根據單元測試快速驗證新修改的程式碼的正確性,換句話說為了方便系統的後期維護升級!

    單元測試某種程度上相當於系統的文件。藉助於檢視單元測試提供的功能和單元測試中如何使用程式單元,開發人員可以直觀的理解程式單元的基礎API,即提高了程式碼的可讀性!

    若是開發流程按照測試驅動開發則先行編寫的單元測試案例就相當於:軟體工程瀑布模式中第二階段——設計階段的文件
    使用測試驅動開發,可以避免實際開發中程式設計人員不完全按照文件規範,因為是基於單元測試設計方法,開發人員不遵循設計要求的解決方案永遠不會通過測試。

  • 什麼時候需要單元測試?

    “單元測試通常被認為是編碼階段的附屬工作。可以在編碼開始之前或原始碼生成之後進行單元測試的設計。”——《軟體工程:實踐者的研究方法》

    對於需要長期維護的專案,單元測試可以說是必須的

    通常來說,程式設計師每修改一次程式就會進行最少一次單元測試,在編寫程式的過程中前後很可能要進行多次單元測試,以保證沒有程式錯誤;雖然單元測試不是必須的,但也不壞,這牽涉到專案管理的政策決定。

  • 單元測試誰來編寫?

    不需要專門的軟體測試人員編寫測試案例,單元測試通常由軟體開發人員編寫。

    也正式因為是開發人員自己寫單元測試部分,也可以讓開發者仔細的思考自己方法和介面是否可以更加便於呼叫

  • 單元測試侷限性

    不能發現整合錯誤、效能問題、或者其他系統級別的問題。單元測試結合其他軟體測試活動更為有效。

  • 單元測試框架

    通常在沒有特定框架支援下,自行建立一個專案作為單元測試專案完全是可行的。
    使用單元測試框架,同時配合編輯器VS,編寫單元測試相對來說會簡單許多。
    .NET下的單元測試框架:MSTest、NUnit




2.單元測試的原則

根本原則:

  • Automatic(自動化)
    單元測試應該是全自動執行的,並且非互動式的
  • Independent
    單元測試方法的執行順序無關緊要
    單元測試的各個方法之間不應該相互依賴
  • Repeatable
    功能程式碼不改的前提下,相同的測試程式碼多次執行,應該得到相同的結果
  • Self-validating
    單元測試方法只有兩個可能的執行結果:通過或失敗,沒有第三種情況。

其他一些規範:

  • 最理想的情況下,應該儘量多寫測試用例,以保證程式碼功能的正確性符合預期,具有良好的容錯性。如果程式碼較複雜,條件分支較多,測試用例最好能覆蓋所有的分支路徑。

  • 實際開發中,沒有必要對每一個函式都進行單元測試。但是若是一個比較獨立的功能(當然也可能這個功能就一個函式),應該對這個功能進行比較詳盡的測試。

  • 單元測試的基本目標:語句覆蓋率達到 70%;核心模組的語句覆蓋率和分支覆蓋率都要達到 100%。

  • 注意一個類中可能有許多方法,我們不是要把所有的方法的單元測試都寫完,在去實現程式碼,而是寫完一個單元測試,就去實現一個方法,是一種快速的迭代

  • 不測試私有方法,因為私有方法不被外部呼叫,測試意義不大,而且你非要測試,那就要使用反射,比較麻煩。

  • 一個測試只測試一個功能




3.單元測試簡單示例

3.1一個簡單的手寫單元測試例項

為了簡潔明瞭的說明什麼是單元測試,首先不使用單元測試框架,自行編寫單元測試專案

比如說新建了一個類Calculator用於對資料的計算,

如下只是隨便的的寫了個方法,方便理解:

public class Calculator
{
    //求一個數的二倍
    public int DoubleValue(int i)
    {
        return i * 2;
    }
}

新建了Calculator類之後,我們編寫單元測試程式碼對該類中方法進行單元測試:
首先新建一個專案,對待測試的方法所在的專案新增引用,

編寫程式碼,測試ClassLib專案中Calculator類中的DoubleValue()方法

測試DoubleValue(int value),該函式是求一個數的二倍,給其一個引數value=2,則期望其得到的結果是4,若是其他值則說明函式編寫是錯誤的,測試不通過。若是該函式的執行結果和期望的結果一樣則執行通過

public static void CalculatorDoubleValueTest()
{  
    //生成一個測試物件的例項
    Calculator obj = new Calculator();
    //設計測試案例
    int value = 2;
    int expected = 4;
        
    //與預期比較
    if (expected == obj.DoubleValue(value))
    {
        Console.WriteLine("測試通過");
    }
    else
    {
        Console.WriteLine($"測試未通過,測試的實際結果是{obj.DoubleValue(value)}");
    }
    Console.ReadKey();
}

通過上面的示例,簡單的演示了單元測試是什麼,但是實際中一般都是使用已有的單元測試框架。而且測試一個方法為了完備性一般都要到所有的邏輯路徑進行測試,所以會對一個方法寫多個測試方法。

3.2單元測試框架MSTest

單元測試一般都是使用現成的單元測試框架,關於.net的單元測試框架有許多,常見的有NUnit,MSTest等等。

這裡使用VS自帶的MStest框架做簡單的演示(一般推薦使用NUnit框架:Undone)

演示的案例,繼續對上述的Calculator類中的DoubleValue()進行單元測試

注意:通常的做法是為每個被測專案建立一個測試專案,為每個被測類建立一個測試類,並且為每個被測方法至少建立一個測試方法。

新建專案--->選擇測試類專案中的單元測試專案,命名為"被測試專案名+Tests"

測試類的命名為“被測試的類+Tests”

測試函式的命名按照 :**[被測方法]_ [測試場景]_[預期行為]** 格式命名

  • 方法名——被測試的方法
  • 測試場景——能產生預期行為的條件
  • 預期行為——在給定條件下,期望被測試方法產生什麼結果

當然在VS中也可以在想要測試的函式上右鍵,建立單元測試,彈出如下視窗,直接點選確定即可,即可生成預設的單元測試程式碼模版

這裡先使用預設自帶的MSTest框架,使用預設的命名格式,會自動生成相應的測試專案和測試函式格式。

編寫單元測試的程式碼,一般按照以下四步編寫:

Arrange:配置測試物件

TestCase:準備測試案例

Act:操作測試物件

Assert:對操作斷言

//注意 [TestClass]和[TestClass()],[TestMethod()]和[TestMethod]寫法等價
namespace ClassLib.Tests
{
    [TestClass()]//通過標註該特性標籤表明該類為測試類
    public class CalculatorTests
    {
        [TestMethod()]//通過標註該特性標籤表明該函式為測試函式
        public void DoubleValueTest_DoubleValue_ReturnTrue()
        {
            //Arrange:準備,例項化一個帶測試的類
            Calculator obj = new Calculator();

            //Test Case:設計測試案例
            int value = 2;
            int expected = 4;

            //Act:執行
            int actual = obj.DoubleValue(value);

            //Assert:斷言
            Assert.AreEqual(expected, actual);
        }
    }
}

點選測試-->執行-->所有測試
或點選測試-->視窗-->測試資源管理器-->執行所有測試

上面執行顯示測試通過顯示的是綠色的標誌,若是測試不通過則會則顯示紅色標誌,在單元測試中有一種“紅綠燈”的概念(你是使用其他的單元測試框架也是同樣的紅綠標誌)。

在測試驅動開發的流程中,就是“紅燈-->修改-->綠燈-->重構-->綠燈”的開發流程。

注意:我是使用的不是VS Enterprise版本故無法直接檢視程式碼的測試覆蓋率,可以使用外掛OpenCover或NCover等其他工具檢視單元測試的覆蓋率。

上面只是演示了怎麼進行一次單元測試,但是實際中我們的測試案例不能僅僅一個,所以要新增多個測試,以提高到測試的完備性

若是對需要大量測試案例的,可以把測試資料存放在專門的用於測試使用的資料庫中,在測試時通過連線資料庫,使用資料庫中的資料進行測試

依舊是上面的示例,把大量的測試案例存放在資料庫

Id                   Input       Expected
-------------------- ----------- -----------
1                    2           4
2                    6           12
3                    13          26
4                    0           0
5                    -2          -4

單元測試的程式碼如下

 public TestContext TestContext { get; set; }//注意為了獲取資料庫的資料,我們要自定義一個TestContext屬性
[TestMethod()]
[DataSource("System.Data.SqlClient",
            @"server=.;database=db_Tome1;uid=sa;pwd=shanzhiming",//資料庫連線字串
            "tb_szmUnitTestDemo",//測試資料存放的表
            DataAccessMethod.Sequential)]//對錶中的資料測試的順序,可以是順序的,也可以是隨機的,這裡是我們選擇順序
public void DoubleValueTest_DoubleValue_ReturnTrue()
{
    //Arrange
    Calculator target = neCalculator();
    //TestCase
    int value = Convert.ToInt(TestContext.DataR["Input"]);
    int expected Convert.ToInt(TestContext.DataR["Expected"]);
    //Act
    int actual target.DoubleValu(value);
    //Assert
    Assert.AreEqual(expected, actual);
}

說明:

  1. 特性標籤[TestClass] [TestMethod]

    MSTest框架通過標籤識別並載入測試

    [TestClass]用來標識包含一個MSTest自動好測試的類,

    [TestMethod]用來標識需要被呼叫的自動化測試的方法

  2. 特性標籤[DataSource]標識用來測試的資料來源,其的引數如下:

    • 第一個引數是providername,即使用的資料來源的名稱空間,其實我們也是可是使用Excel表格的(選單“專案”-->新增新的資料來源……)參考:CSDN:vs2015資料驅動的單元測試

      providername值參考:

      • "system.data.sqlclient" ----說明使用的是mssqlserver資料庫

      • "system.data.sqllite" ----說明使用的是sqllite資料庫

      • "system.data.oracleclient" ----說明使用的是oracle資料庫或

      • "mysql.data.mysqlclient" ----說明使用的是mysql資料庫

    • 第二個引數是connectionString,我習慣是這樣寫:

      @"server=.;database=資料庫;uid=使用者ID;pwd=密碼"

      但是推薦這樣寫:

      @"Data Source=localhost;Initial Catalog=資料庫;User ID=使用者ID;Password=密碼"

    • 第三個引數是tablename,選擇使用的資料庫中的哪張表

    • 第四個引數確定對錶中的資料測試的順序.
      可以是順序的:DataAccessMethod.Sequential
      可以是隨機的:DataAccessMethod.Random




4.單元測試框架特性標籤

在MSTest單元測試框架中主要有以下的一些特性標籤:

(參考)

MS Test Attribute 用途
[TestClass] 定義一個測試類,裡面可以包含很多測試函式和初始化、銷燬函式(以下所有標籤和其他斷言)。
[TestMethod] 定義一個獨立的測試函式。
[ClassInitialize] 定義一個測試類初始化函式,每當執行測試類中的一個或多個測試函式時,這個函式將會在測試函式被呼叫前被呼叫一次(在第一個測試函式執行前會被呼叫)。
[ClassCleanup] 定義一個測試類銷燬函式,每當測試類中的選中的測試函式全部執行結束後執行(在最後一個測試函式執行結束後執行)。
[TestInitialize] 定義測試函式初始化函式,每個測試函式執行前都會被呼叫一次。
[TestCleanup] 定義測試函式銷燬函式,每個測試函式執行完後都會被呼叫一次。
[AssemblyInitialize] 定義測試Assembly初始化函式,每當這個Assembly中的有測試函式被執行前,會被呼叫一次(在Assembly中第一個測試函式執行前會被呼叫)。
[AssemblyCleanup] 定義測試Assembly銷燬函式,當Assembly中所有測試函式執行結束後,執行一次。(在Assembly中所有測試函式執行結束後被呼叫)
[Ignore] 跳過(忽略)該測試函式
[TestCategory("測試類別")] 給測試自定義分類,便於有選擇的執行指定類別的單元測試

說明:

  • 使用[ClassInitialize][ClassCleanup]標籤特性

    可以在測試之前或之後方便地控制測試的初始化和清理,從而確保所有的測試都是使用新的未更改的狀態。

    注意,這是很有必要的,可以有效的防止測試失敗是因為測試之間的依賴性導致失敗。

    注意兩個標籤特性需要放在一個無返回值的靜態方法上,

    且標註[ClassInitialize]特性的方法的引數是:TestContext testcontext

    示例:比如說在一個測試類初始化一個測試物件,並在測試完成後釋放,程式碼如下:


[TestClass()]
public class CalculatorTests
{
    //使用ClassInitialize標籤初始化一個Calculator物件以供下面所有的測試([ClassCleanup]之前)使用
    private static Calculator calc = null;
    [ClassInitialize]
    public static  void  ClassInit(TestContext testcontext)
    {
        calc = new Calculator();
    }

    [TestMethod()]
    public void testMethod1()
    {
         //測試
    }
    [TestMethod()]
    public void testMethod2()
    {
        //測試
    }
    [TestMethod()]
    public void testMethod3()
    {
        //測試
    }
     
    [ClassCleanup]
    public static  void Classup()
    {
        calc = null;
    }
}




5.單元測試中的斷言Assert

  1. 斷言是什麼?可以從字面理解是“十分肯定的說”,在程式設計中可以通過 不同的斷言來測試方法實際執行的結果和你期望的結果是否一致。

  2. 斷言是單元測試最基本的組成部分,Assert類的靜態方法提供了不同形式的多種斷言。
    MStest中Assert的常用靜態方法:(參考):

    MS Test Assert 用途
    Assert.AreEqual() 驗證值相等
    Assert.AreNotEqual() 驗證值不相等
    Assert.AreSame() 驗證引用相等
    Assert.AreNotSame() 驗證引用不相等
    Assert.Inconclusive() 暗示條件還未被驗證
    Assert.IsTrue() 驗證條件為真
    Assert.IsFalse() 驗證條件為假
    Assert.IsInstanceOfType() 驗證例項匹配型別
    Assert.IsNotInstanceOfType() 驗證例項不匹配型別
    Assert.IsNotNull() 驗證條件為NULL
    Assert.IsNull() 驗證條件不為 NULL
    Assert.Fail() 驗證失敗
  3. 針對字串的斷言,使用StringAssert的靜態方法:

    注意可以根據VS的只能提示自行檢視StringAssert的所有靜態方法,或是檢視StringAssert的定義,可以檢視其所有的靜態方法

    詳細使用可參考

    StringAssert 用途
    StringAssert.AreEqualIgnoringCase(string expected,string actual) 用於斷言 兩個字串在不區分大小寫情況下是否相等,需要提供兩個參 數,第一個是期待的結果,第二個是實際結果.
    StringAssert.Contains() 用於斷言一個字串是否包含另一字串,其中第一個引數為被包含的字串,第二個為實際字串
    StringAssert.StartsWith() 斷言字串是否以某(幾)字元開始, 第一個引數為開頭的字串 ,第二個為實際字串
    StringAssert.EndsWith() 斷言字串是否以某(幾)字元結束
    StringAssert.Matches() 斷言字串是否符合特定的正則表示式
  4. 針對集合的斷言,使用CollectionAssert的靜態方法:

    注意可以根據VS的只能提示自行檢視CollectionAssert所有的靜態方法,或是檢視CollectionAssert的定義,可以檢視其所有的靜態方法

    詳細使用可參考

    CollectiongAssert 用途
    CollectionAssert.AllItemsAreNotNull 斷言集合裡的元素全部不是Null,也即集合不包含null元素,這個方法只有一個引數,傳入我們要判斷的集合即可
    CollectionAssert.AllItemsAreUnique 斷言集合裡面的元素全部是惟一的,即集合裡沒有重複元素.
    CollectionAssert.AreEqual 用於斷言兩個集合是否相等
    CollectionAssert.AreEquivalent 用來判斷兩個集合的元素是否等價,如果兩個集合元素型別相同,個數也相同,即視為等價,與上面的AreEqual方法相比,它不關心順序
    CollectionAssert.Contains 斷言集合是否包含某一元素
    CollectionAssert.IsEmpty 斷言某一集合是空集合,即元素個數為0
    CollectionAssert.IsSubsetOf 判斷一個集合是否為另一個集合的子集,這兩個集合不必是同一類集合(可以一個是array,一個是list),只要一個集合的元素完全包含在另一個集合中,即認為它是另一個集合的子集




6.單元測試中驗證預期的異常

若是程式中在某種特定的條件下有異常丟擲,為了進行單元測試,我們設計指定的測試案例,期望在該測試案例程式丟擲異常,並檢驗其是否丟擲異常。

簡單示例:

/// <summary>
/// 計算從from到to的所有整數的和
/// </summary>
public int Sum(int from, int to)
{
    if (from > to)
    {
        throw new ArgumentException("引數from必須小於to");
    }
    int sum = 0;
    for (int i = from; i <= to; i++)
    {
        sum += i;
    }
    return sum;
}

在程式中,若是引數from >to則丟擲異常new ArgumentException("引數from必須小於to");

為了檢驗該程式在該條件下是否真的會丟擲異常,可以創造測試案例from=100 > to=50
期望Sum()函式程式碼中執行:throw new ArgumentException("引數from必須小於to");,所以我們要測試期望丟擲的異常ArgumentException

使用標籤[ExpectedException(typeof(“丟擲的異常物件”))]

單元測試程式碼:

       
//異常測試,新增ExpectedException
[TestMethod]
[ExpectedException(type(ArgumentException))]
public void SumTest_ArgumentException_TrowException()
{
    Calculator bjCalcultor = new Calculator();
    int from=100,to=50;
    calc.Sum(from, to);
}

因為程式丟擲了我們期望的異常,所以該測試通過。如若程式沒有丟擲該異常則測試失敗。




7.單元測試中針對狀態的間接測試

  • 基於狀態的測試(也稱狀態驗證),是指在方法執行之後,通過檢查被測系統及其協作者(依賴項)的狀態來檢測該方法是否正確工作。

  • 簡單示例:

    下面的方法isLastFilenameValid(string filename)在執行後會改變類中屬性wasLastFileNameValid的值

//用於儲存狀態的結果用於以後的驗證
public bool wasLastFileNameValid { get; set; }
//判斷輸入的字串是否是.txt檔名
public bool isLastFilenameValid(string filename)
{
   if (!(filename .ToLower()).EndsWith("txt"))
   {
       wasLastFileNameValid = false;
       return false ;
   }
   else
   {
       wasLastFileNameValid = true;
       return true;
   }
} 
  • 單元測試函式:

    該測試是測試isLastFilenameValid(),

    因為該函式是把結果賦值給類中屬性wasLastFileNameValid,

    所以此處驗證的是Calculator類中屬性wasLastFileNameValid是否符合我們的期望,

    而不是簡單的驗證isLastFilenameValid()的返回值是否符合我們的期望。

[TestMethod()]
public void isLastFilenameValid_ValidName_ReturnTrue()
{
    Calculator calc = new Calculator();
    string fileName = "test.txt";
    calc.isLastFilenameValid(fileName)
    Assert.IsTrue(calc.wasLastFileNameValid);
  
}




8.單元測試在MVC模式中的實現

參考

  • 因為MVC模式中的Controller類中的Action的返回值是和普通類的方法不一樣的,

    Action的返回值是ActionResult型別的,其子類又有許多,

    具體怎麼實現對MVC模式的單元測試呢?請看一個簡單的示例:

    程式碼背景:在一個MVC專案中的HomeController控制器中有一個Action是Index()

    首先先定義一個Person類其中有Id和Name兩個屬性

    Action如下:

      public class HomeController : Controller
      {
          // GET: Home
          public ActionResult Index()
          {
              return View("Index",new Person { Id = 001, Name = "shanzm" });
          }
      }
    

    對上面的HomeController中的Index()進行一個簡單的單元測試

    新建一個單元測試專案(或者在建立MVC專案的時候選中單元測試的按鈕,則自動生成一個單元測試專案)

    注意一定要先安裝MVC的程式集,NuGet:Install-Package Microsoft.AspNet.Mvc -Version 5.2.3

      [TestMethod()]
      public void Index_Index_ReturnTrue()
      {
          //Arrage:準備測試物件
          HomeController hcont = new HomeController();
          //Act:執行測試函式
          ViewResult  result =(ViewResult)hcont.Index();
          var viewName = result.ViewName;
          Person model = (Person)result.Model;
          //Assert:斷言符合期望
          Assert.IsTrue(viewName == "Index" && model.Id == 001 && model.Name == "shanzm"&& );
      }

說明:

  1. 如果View()函式沒指定檢視,而是使用預設的檢視,則檢視名為空,所以如果名稱不寫的時候我們可以斷言ViewName是空。

  2. 注意在Action中的ViewBag傳遞的資料在單元測試中需要通過ViewData方式獲取(因為ViewBag是對ViewData的動態封裝,在同一個Action中二者資料相通,此乃ASP.NET MVC的基礎,不詳述)

  3. 其實呀,MVC模式作為UI層,有許多東西其實是很難(但不是不可以)模擬物件去進行單元測試的,一般其實不推薦做過多的單元測試。(注意不是不做,是不做過多過複雜的單元測試)




8.單元測試相關參考

書籍:.NET 單元測試的藝術

書籍:單元測試之道C#版

微軟:dotnet文件

部落格園:對比MS Test與NUnit Test框架

部落格園:.net持續整合測試篇之Nunit檔案斷言、字串斷言及集合斷言

部落格園:.netcore持續整合測試篇之MVC層單元測試




9.示例原始碼下載

示例原始碼下載