C#單元測試,帶你快速入門

来源:http://www.cnblogs.com/zhaopei/archive/2017/05/17/UnitTesting.html
-Advertisement-
Play Games

註:本文示例環境 VS2017 XUnit 2.2.0 單元測試框架 xunit.runner.visualstudio 2.2.0 測試運行工具 Moq 4.7.10 模擬框架 為什麼要編寫單元測試 對於為什麼要編寫單元測試,我想每個人都有著自己的理由。對於我個人來說,主要是為了方便修改(bug修 ...


註:本文示例環境

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

為什麼要編寫單元測試

對於為什麼要編寫單元測試,我想每個人都有著自己的理由。對於我個人來說,主要是為了方便修改(bug修複)而不引入新的問題。可以放心大膽的重構,我認為重構覺得是提高代碼質量和提升個人編碼能力的一個非常有用的方式。好比一幅名畫一尊雕像,都是作者不斷重繪不斷打磨出來的,而優秀的代碼也需要不斷的重構。
當然好處不僅僅如此。TDD驅動,使代碼更加註重介面,迫使代碼減少耦合,使開發人員一開始就考慮面對各種情況編寫代碼,一定程度的保證的代碼質量,通過測試方法使後續人員快速理解代碼...等。
額,至於不寫單元測試的原因也有很多。原因無非就兩種:懶、不會。當然你還會找更多的理由的。

框架選型

至於框架的選型。其實本人並不瞭解也沒寫過單元測試,這算是第一次真正接觸吧。在不瞭解的情況下怎麼選型呢?那就是看哪個最火、用的人多就選哪個。起碼出了問題也容易同別人交流。

  • 單元測試框架:XUnit 2.2.0。asp.net mvc就是用的這個,此內框架還有:NUnit、MSTest等。
  • 測試運行工具:xunit.runner.visualstudio 2.2.0。類似如:Resharper的xUnit runner插件。
  • 模擬框架:Moq 4.7.10。 asp.net mvc、Orchard使用了。此類框架還有:RhinoMocks、NSubstitute、FakeItEasy等。

基本概念

  • AAA邏輯順序
    • 準備(Arrange)對象,創建對象,進行必要的設置
    • 操作(Act)對象
    • 斷言(Assert)某件事情是預期的。
  • Assert(斷言):對方法或屬性的運行結果進行檢測
  • Stub(測試存根\樁對象):用返回指定結果的代碼替換方法(去偽造一個方法,阻斷對原來方法的調用,為了讓測試對象可以正常的執行)
  • Mock(模擬對象):一個帶有期望方法被調用的存根(可深入的模擬對象之間的交互方式,如:調用了幾次、在某種情況下是否會拋出異常。mock是一種功能豐富的stub)
    Stub和Mock的定義比較抽象不好理解,延伸閱讀1閱讀2閱讀3

好的測試

  • 測試即文檔
  • 無限接近言簡意賅的自然化語言
  • 測試越簡明越好,每個測試只關註一個點。
  • 好的測試足夠快,測試易於編寫,減少依賴
  • 好的測試應該相互隔離,不依賴於別的測試,不依賴於外部資源
  • 可描述的命名:UnitOfWorkName_ScenarioUnderTest_ExpectedBehavior(命名可團隊約定,我甚至覺得中文命名也沒什麼不可以的)
    • UnitOfWorkName  被測試的方法、一組方法或者一組類
    • Scenario  測試進行的假設條件,例如“登入失敗”,“無效用戶”或“密碼正確”等
    • ExpectedBehavior  在測試場景指定的條件下,你對被測試方法行為的預期  

基礎實踐

“廢話”說的夠多了,下麵擼起袖子開乾吧。
下麵開始準備工作:

  • vs2017新建一個空項目 UnitTestingDemo
  • 新建類庫 TestDemo (用於編寫被測試的類)
  • 新建類庫 TestDemo.Tests (用於編寫單元測試)
  • 對類庫 TestDemo.Tests 用nuget 安裝XUnit 2.2.0、xunit.runner.visualstudio 2.2.0、Moq 4.7.10。
  • 添加 TestDemo.Tests 對 TestDemo 的引用。

例:

public class Arithmetic
{
    public int Add(int nb1, int nb2)
    {
        return nb1 + nb2;
    }
}

對應的單元測試:(需要導入using Xunit;命名空間。 )

public class Arithmetic_Tests
{
    [Fact]//需要在測試方法加上特性Fact
    public void Add_Ok()
    {
        Arithmetic arithmetic = new Arithmetic();
        var sum = arithmetic.Add(1, 2);
        
        Assert.True(sum == 3);//斷言驗證
    }
}

一個簡單的測試寫好了。由於我們使用的vs2017 它出了一個新的功能“Live Unit Testing”,我們可以啟用它進行實時的測試。也就是我們編輯單元測試,然後保存的時候,它會自動生成自動測試,最後得出結果。


我們看到了驗證通過的綠色√。
註意到測試代碼中的參數和結果都寫死了。如果我們要對多種情況進行測試,豈不是需要寫多個單元測試方法或者進行多次方法執行和斷言。這也太麻煩了。在XUnit框架中為我們提供了Theory特性。使用如下:
例:

[Theory]
[InlineData(2, 3, 5)]
[InlineData(2, 4, 6)]
[InlineData(2, 1, 3)] //對應測試方法的形參
public void Add_Ok_Two(int nb1, int nb2, int result)
{
    Arithmetic arithmetic = new Arithmetic();
    var sum = arithmetic.Add(nb1, nb2);
    Assert.True(sum == result);
}


測試了正確的情況,我們也需要測試錯誤的情況。達到更好的覆蓋率。
例:

[Theory]
[InlineData(2, 3, 0)]
[InlineData(2, 4, 0)]
[InlineData(2, 1, 0)] 
public void Add_No(int nb1, int nb2, int result)
{
    Arithmetic arithmetic = new Arithmetic();
    var sum = arithmetic.Add(nb1, nb2);
    Assert.False(sum == result);
}

有時候我們需要確定異常
例:

public int Divide(int nb1, int nb2)
{
    if (nb2==0)
    {
        throw new Exception("除數不能為零");
    }
    return nb1 / nb2;
}
[Fact]      
public void Divide_Err()
{
    Arithmetic arithmetic = new Arithmetic(); 
    Assert.Throws<Exception>(() => { arithmetic.Divide(4, 0); });//斷言 驗證異常
}

以上為簡單的單元測試。接下來,我們討論更實際更真實的。
我們一般的項目都離不開資料庫操作,下麵就來實踐下對EF使用的測試:

  • 使用nuget安裝 EntityFramework 5.0.0

例:

public class StudentRepositories
{
    //...
    public void Add(Student model)
    {
        db.Set<Student>().Add(model);
        db.SaveChanges();
    }
}
[Fact]
public void Add_Ok()
{
    StudentRepositories r = new StudentRepositories();
    Student student = new Student()
    {
        Id = 1,
        Name = "張三"
    };
    r.Add(student);

    var model = r.Students.Where(t => t.Name == "張三").FirstOrDefault();
    Assert.True(model != null);           
}

我們可以看到我們操作的是EF連接的實際庫。(註意:要改成專用的測試庫)
我們會發現,每測試一次都會產生對應的垃圾數據,為了避免對測試的無干擾性。我們需要對每次測試後清除垃圾數據。

//註意:測試類要繼承IDisposable介面
public void Dispose()
{
 StudentRepositories r = new StudentRepositories();
 var models = r.Students.ToList();
 foreach (var item in models)
 {
     r.Delete(item.Id);
 }
}

這樣每執行一個測試方法就會對應執行一次Dispose,可用來清除垃圾數據。
我們知道對資料庫的操作是比較耗時的,而單元測試的要求是儘可能的減少測試方法的執行時間。因為單元測試執行的比較頻繁。基於前面已經對資料庫的實際操作已經測試過了,所以我們在後續的上層操作使用Stub(存根)來模擬,而不再對資料庫進行實際操作。
例:
我們定義一個介面IStudentRepositories 併在StudentRepositories 繼承。

 public interface IStudentRepositories
 {
     void Add(Student model);
 }
 public class StudentRepositories: IStudentRepositories
 {
    //省略。。。 (還是原來的實現)
 }   
public class StudentService
{
    IStudentRepositories studentRepositories;
    public StudentService(IStudentRepositories studentRepositories)
    {
        this.studentRepositories = studentRepositories;
    }
    public bool Create(Student student)
    {
        studentRepositories.Add(student);

        return true;
    }
}

新建一個類,用來測試。這個Create會使用倉儲操作資料庫。這裡不希望實際操作資料庫,以達到快速測試執行。

[Fact]
public void Create_Ok()
{
    IStudentRepositories studentRepositories = new StubStudentRepositories();
    StudentService service = new StudentService(studentRepositories);
    var isCreateOk = service.Create(null);
    Assert.True(isCreateOk);
}

public class StubStudentRepositories : IStudentRepositories
{
    public void Add(Student model)
    {
    }
}


圖解:

每次做類似的操作都要手動建議StubStudentRepositories存根,著實麻煩。好在Mock框架(Moq)可以自動幫我們完成這個步驟。
例:

[Fact]
public void Create_Mock_Ok()
{
    var studentRepositories = new Mock<IStudentRepositories>();
    var notiy = new Mock<Notiy>();
    StudentService service = new StudentService(studentRepositories.Object);
    var isCreateOk = service.Create(null);
    Assert.True(isCreateOk);
}

相比上面的示例,是不是簡化多了。起碼代碼看起來清晰了,可以更加註重測試邏輯。

下麵接著來看另外的情況,並且已經通過了測試

public class Notiy
{
    public bool Info(string messg)
    {
        //發送消息、郵件發送、簡訊發送。。。
        //.........
        if (string.IsNullOrWhiteSpace(messg))
        {
            return false;
        }
        return true;
    }
}
public class Notiy_Tests
{
    [Fact]
    public void Info_Ok()
    {
        Notiy notiy = new Notiy();
        var isNotiyOk = notiy.Info("消息發送成功");
        Assert.True(isNotiyOk);
    }
}

現在我們接著前面的Create方法加入消息發送邏輯。

public bool Create(Student student)
{
    studentRepositories.Add(student);

    var isNotiyOk = notiy.Info("" + student.Name);//消息通知

    //其他一些邏輯
    return isNotiyOk;
}
[Fact]
public void Create_Mock_Notiy_Ok()
{
    var studentRepositories = new Mock<IStudentRepositories>();
    var notiy = new Mock<Notiy>();
    StudentService service = new StudentService(studentRepositories.Object, notiy.Object);
    var isCreateOk = service.Create(new Student());
    Assert.True(isCreateOk);
}

而前面我們已經對Notiy進行過測試了,接下來我們不希望在對Notiy進行耗時操作。當然,我們可以通過上面的Mock框架來模擬。這次和上面不同,某些情況我們不需要或不想寫對應的介面怎麼來模擬?那就使用另外一種方式把要測試的方法virtual。
例:

public virtual bool Info(string messg)
{
    //發送消息、郵件發送、簡訊發送。。。
    //.........
    if (string.IsNullOrWhiteSpace(messg))
    {
        return false;
    }
    return true;
}

測試如下

[Fact]
public void Create_Mock_Notiy_Ok()
{
    var studentRepositories = new Mock<IStudentRepositories>();
    var notiy = new Mock<Notiy>();
    notiy.Setup(f => f.Info(It.IsAny<string>())).Returns(true);//【1】
    StudentService service = new StudentService(studentRepositories.Object, notiy.Object);
    var isCreateOk = service.CreateAndNotiy(new Student());
    Assert.True(isCreateOk);
}

我們發現了標註【1】處的不同,這個代碼的意思是,執行模擬的Info方法返回值為true。參數It.IsAny() 是任意字元串的意思。
當然你也可以對不同參數給不同的返回值:

notiy.Setup(f => f.Info("")).Returns(false);
notiy.Setup(f => f.Info("消息通知")).Returns(true);

有時候我們還需要對private方法進行測試

  • 使用nuget 安裝 MSTest.TestAdapter 1.1.17
  • 使用nuget 安裝 MSTest.TestFramework 1.1.17

例:

private bool XXXInit()
{
    return true;
}
[Fact]
public void XXXInit_Ok()
{
    var studentRepositories = new StudentService();
    var obj = new Microsoft.VisualStudio.TestTools.UnitTesting.PrivateObject(studentRepositories);
    Assert.True((bool)obj.Invoke("XXXInit"));
}

如果方法有參數,接著Invoke後面傳入即可。

好了,就說這麼多吧。只能說測試的內容還真多,想要一篇文章說完是不可能的。但希望已經帶你入門了。

附錄

xUnit(2.0) 斷言 (來源)

  • Assert.Equal() 驗證兩個參數是否相等,支持字元串等常見類型。同時有泛型方法可用,當比較泛型類型對象時使用預設的IEqualityComparer實現,也有重載支持傳入IEqualityComparer
  • Assert.NotEqual() 與上面的相反
  • Assert.Same() 驗證兩個對象是否同一實例,即判斷引用類型對象是否同一引用
  • Assert.NotSame() 與上面的相反
  • Assert.Contains() 驗證一個對象是否包含在序列中,驗證一個字元串為另一個字元串的一部分
  • Assert.DoesNotContain() 與上面的相反
  • Assert.Matches() 驗證字元串匹配給定的正則表達式
  • Assert.DoesNotMatch() 與上面的相反
  • Assert.StartsWith() 驗證字元串以指定字元串開頭。可以傳入參數指定字元串比較方式
  • Assert.EndsWith() 驗證字元串以指定字元串結尾
  • Assert.Empty() 驗證集合為空
  • Assert.NotEmpty() 與上面的相反
  • Assert.Single() 驗證集合只有一個元素
  • Assert.InRange() 驗證值在一個範圍之內,泛型方法,泛型類型需要實現IComparable,或傳入IComparer
  • Assert.NotInRange() 與上面的相反
  • Assert.Null() 驗證對象為空
  • Assert.NotNull() 與上面的相反
  • Assert.StrictEqual() 判斷兩個對象嚴格相等,使用預設的IEqualityComparer對象
  • Assert.NotStrictEqual() 與上面相反
  • Assert.IsType()/Assert.IsType() 驗證對象是某個類型(不能是繼承關係)
  • Assert.IsNotType()/Assert.IsNotType() 與上面的相反
  • Assert.IsAssignableFrom()/Assert.IsAssignableFrom() 驗證某個對象是指定類型或指定類型的子類
  • Assert.Subset() 驗證一個集合是另一個集合的子集
  • Assert.ProperSubset() 驗證一個集合是另一個集合的真子集
  • Assert.ProperSuperset() 驗證一個集合是另一個集合的真超集
  • Assert.Collection() 驗證第一個參數集合中所有項都可以在第二個參數傳入的Action序列中相應位置的Action上執行而不拋出異常。
  • Assert.All() 驗證第一個參數集合中的所有項都可以傳入第二個Action類型的參數而不拋出異常。與Collection()類似,區別在於這裡Action只有一個而不是序列。
  • Assert.PropertyChanged() 驗證執行第三個參數Action使被測試INotifyPropertyChanged對象觸發了PropertyChanged時間,且屬性名為第二個參數傳入的名稱。
  • Assert.Throws()/Assert.Throws()Assert.ThrowsAsync()/Assert.ThrowsAsync() 驗證測試代碼拋出指定異常(不能是指定異常的子類)如果測試代碼返回Task,應該使用非同步方法
  • Assert.ThrowsAny() 驗證測試代碼拋出指定異常或指定異常的子類
  • Assert.ThrowsAnyAsync() 如果測試代碼返回Task,應該使用非同步方法

Moq(4.7.10) It參數約束

  • Is:匹配確定的給定類型
  • IsAny:匹配給定的任何值
  • IsIn: 匹配指定序列中存在的任何值
  • IsNotIn: 匹配指定序列中未找到的任何值
  • IsNotNull: 找任何值的給定值類型,除了空
  • IsInRange:匹配給定類型的範圍
  • IsRegex:正則匹配

相關資料

相關推薦

demo


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近項目需求,功能變數名稱備案主體變更,要去香港搭個伺服器中轉,於是就很蛋疼的開始伺服器搭建之路。 在php配置時候出現一個問題,代碼提示 我以為是pdo沒有裝,然後就開始裝pdo,編譯後一直報錯,然後看論壇一群人抄啊抄的什麼都不知道,還抄錯的,讓人惱火。然後stackoverflow上有人說,pdo是ph ...
  • 我的電腦是在Windows 10下安裝的Ubuntu 14.04雙系統,今天進入Ubuntu系統訪問Windows 10 磁碟,出現如下錯誤: Error mounting /dev/sda1 at /media/linuxidc/WIN7: Command-line `mount -t "ntfs ...
  • 1、在vmware虛擬機選項下,選擇安裝vmware-tools 2、將vmware安裝目錄下的linux.iso裝載到系統中 2.1、選擇需安裝VMWareTools的虛擬機,右擊--可移動設備--CD/DVD--設置 2.2、選擇CD/DVD(SATA)--使用ISO映像文件--選文件--打鉤設 ...
  • 微軟在去年發佈了Bash On Windows, 這項技術允許在Windows上運行Linux程式, 我相信已經有很多文章解釋過Bash On Windows的原理, 而今天的這篇文章將會講解如何自己實現一個簡單的原生Linux程式運行器, 這個運行器在用戶層實現, 原理和Bash On Windo ...
  • 工具: 1、8G或以上U盤一枚; 2、CDlinux0.9.7.1鏡像文件,註意其他版本不一定能成功(傳送門http://pan.baidu.com/s/1o7P6Gu2); 3、UltraISO或Unetbootin(傳送門http://pan.baidu.com/s/1mhUuzqw); 4、B ...
  • 1.原因 由於最近對於非同步connect函數的測試,發現提前將一個套接字加入epoll監聽隊列會不斷爆出epollhup事件 2.示例 ........ iEpoll = epoll_create(1); iFd = socket(AF_INET, SOCK_STREAM, 0); stEvent. ...
  • 操作要領:封閉埠,杜絕網路病毒對這些埠的訪問權,以保障電腦安全,減少病毒對上網速度的影響。 近日發現有些人感染了新的網路蠕蟲病毒,該病毒使用衝擊波病毒專殺工具無法殺除,請各位儘快升級電腦上的殺毒軟體病毒庫,在斷開電腦網路連接的情況下掃描硬碟,查殺病毒。安裝了防火牆軟體的用戶,請 封閉 TC ...
  • Local系統管理員新增了一個VG,將一個原掛載點/u02改為了/u02-old, 如下所示。 [root@mylnx01 ~]# df -hFilesystem Size Used Avail Use% Mounted on/dev/mapper/VolGroup00-LogVol00 37G 2... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...