歡迎光臨
每天分享高質量文章

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

來自:農碼一生

鏈接:https://www.cnblogs.com/zhaopei/p/UnitTesting.html

註:本文示例環境

 

  • 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(http://ruby-china.org/topics/10977)、

    閱讀2(http://www.cnblogs.com/chaogex/p/3388386.html?utm_source=tuicool&utm;_medium=referral)、

    閱讀3(http://www.cnblogs.com/happyframework/p/3595547.html)

 

好的測試

 

  • 測試即文件

  • 無限接近言簡意賅的自然化語言

  • 測試越簡明越好,每個測試只關註一個點。

  • 好的測試足夠快,測試易於編寫,減少依賴

  • 好的測試應該相互隔離,不依賴於別的測試,不依賴於外部資源

  • 可描述的命名: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(() => { arithmetic.Divide(4, 0); });//斷言 驗證異常

}

 

以上為簡單的單元測試。接下來,我們討論更實際更真實的。

 

我們一般的專案都離不開資料庫操作,下麵就來實踐下對EF使用的測試:

使用nuget安裝 EntityFramework 5.0.0

 

例:

 

public class StudentRepositories

{

    //…

    public void Add(Student model)

    {

        db.Set().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();

    var notiy = new Mock();

    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();

    var notiy = new Mock();

    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();

    var notiy = new Mock();

    notiy.Setup(f => f.Info(It.IsAny())).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() 與上面的相反

  • Asert.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:正則匹配

相關資料

Moq(Mock框架):

 

  • http://www.cnblogs.com/haogj/archive/2011/07/22/2113496.html

  • http://www.cnblogs.com/jams742003/archive/2010/03/02/1676215.html

 

NSubstitute(Mock框架):

 

  • http://www.cnblogs.com/gaochundong/archive/2013/05/22/nsubstitute_manual.html

 

Shouldly(方便斷言書寫):

 

http://www.cnblogs.com/defzhu/p/4841289.html

Effort.EF6:通過nuget獲取,使得創建一個偽造的、供EF容易使用的記憶體資料庫成為可能。

 

netDumbster:通過nuget獲取netDumbster組件,該組件提供了SimpleSmtpServer物件用於模擬郵件發送環境

 

HttpSimulator:通過nuget獲取,通過使用HttpSimulator物件發起Http請求,在其生命周期內HttContext物件為可用狀態

 

相關書籍:《單元測試之道》、《C#測試驅動開發》、《測試驅動開發》、《單元測試藝術》

 

相關推薦

 

http://www.cnblogs.com/easygame/p/5199785.html

http://www.cnblogs.com/edisonchou/p/5447812.html

http://www.cnblogs.com/lsxqw2004/p/4793623.html

 

demo

 

https://github.com/zhaopeiym/BlogDemoCode/tree/master/UnitTestingDemo

赞(0)

分享創造快樂