關於C#程序的單元測試

志銘-2020年1月23日 11:49:41

1.單元測試概念

  • 什麼是單元測試?mysql

    單元測試(unit testing)是一段自動化的代碼,用來調用被測試的方法或類,然後驗證基於該方法或類的邏輯行爲的一些假設。git

    簡而言之說:單元測試是一段代碼(一般一個方法)調用另一段代碼,隨後檢驗一些假設的正確性。程序員

    在過程化編程中,一個單元就是單個程序、函數、過程等;github

    對於面向對象編程,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法。正則表達式

  • 爲何要單元測試?sql

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

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

    單元測試某種程度上至關於系統的文檔。藉助於查看單元測試提供的功能和單元測試中如何使用程序單元,開發人員能夠直觀的理解程序單元的基礎API,即提升了代碼的可讀性!mvc

    如果開發流程按照測試驅動開發則先行編寫的單元測試案例就至關於:軟件工程瀑布模式中第二階段——設計階段的文檔
    使用測試驅動開發,能夠避免實際開發中編程人員不徹底按照文檔規範,由於是基於單元測試設計方法,開發人員不遵循設計要求的解決方案永遠不會經過測試。

  • 何時須要單元測試?

    「單元測試一般被認爲是編碼階段的附屬工做。能夠在編碼開始以前或源代碼生成以後進行單元測試的設計。」——《軟件工程:實踐者的研究方法》

    對於須要長期維護的項目,單元測試能夠說是必須的

    一般來講,程序員每修改一次程序就會進行最少一次單元測試,在編寫程序的過程當中先後極可能要進行屢次單元測試,以保證沒有程序錯誤;雖然單元測試不是必須的,但也不壞,這牽涉到項目管理的政策決定。

  • 單元測試誰來編寫?

    不須要專門的軟件測試人員編寫測試案例,單元測試一般由軟件開發人員編寫。

    也正式由於是開發人員本身寫單元測試部分,也可讓開發者仔細的思考本身方法和接口是否能夠更加便於調用

  • 單元測試侷限性

    不能發現集成錯誤、性能問題、或者其餘系統級別的問題。單元測試結合其餘軟件測試活動更爲有效。

  • 單元測試框架

    一般在沒有特定框架支持下,自行建立一個項目做爲單元測試項目徹底是可行的。
    使用單元測試框架,同時配合編輯器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.示例源代碼下載

示例源代碼下載

相關文章
相關標籤/搜索