關於C#程序的單元測試


志銘-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.示例源代碼下載

點擊下載示例代碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM