2015年11月3日 星期二

編寫單元測試時的好用輔助套件 - Fluent Assertions

twMVC#12 由 91 哥所分享的「如何在實務上使用 TDD 來開發」開始,然後 91 哥在 SkillTree 講授「自動測試與 TDD 實務開發(使用C#)」四個梯次,從一開始對測試與開發的懵懵懂懂,到現在雖然還無法相當進階,但也已經在平常工作的開發裡導入單元測試,並且向部門同事分享,分享的內容是以 91 哥在課堂上的內容為基礎,再以公司團隊的現況去做調整,我無法像 91 哥那樣可以將測試、開發甚至到後續的規格實例化以及使用 BDD 等一連串內容講得鉅細靡遺,所以我就以我本身的專案導入現況以及每次推進的內容逐一分享給同事。

這一篇是介紹「Fluent Assertions」這個好用的測試輔助套件,原本早就規劃在部落格文章的寫作時程裡,但是這一年來除了上半年因為工作忙碌而影響了寫文進度,另一方面還是因為變得有點懶,所以文章產量比起以往銳減許多,就在「自動測試與 TDD 實務開發(使用C#)」第四梯次課程裡被 91 哥點名了好多次,因為在 Facebook 有時會對於目前使用了哪些工具、套件而寫些感想,所以讓 91 哥覺得我有寫些工具套件的介紹文,不過既然被 91 哥點名了,這也提醒我要趕快把這些文章給補一補。

 


平常我們在編寫單元測試程式碼的時候,大多會遵循 3A 原則,依次以 Arrange, Act, Assert 的方式去寫單元測試的程式,例如以下:

image

像這種簡單的單元測試在寫 Assert 的部分還不會覺得有什麼不便,但如果你的單元測試會有可能在 Assert 做多種驗證的時候,使用基本的 Assert 操作就會覺得有些繁瑣,例如以下的單元測試:

image

 

MsTest 的 Assert, CollectionAssert 類別

MSDN - Assert 類別 (Microsoft.VisualStudio.TestTools.UnitTesting)
https://msdn.microsoft.com/zh-tw/library/microsoft.visualstudio.testtools.unittesting.assert.aspx

常用到的有:

Assert.AreEqual(expected, actual);
Assert.AreNotEqual(expected, actual);
Assert.IsTrue(actual);
Assert.IsFalse(actual);
Assert.IsNull(actual);
Assert.IsNotNull(actual);
Assert.IsInstanceOfType(actual, typeof(expectedType));
Assert.IsNotInstanceOfType(actual, typeof(wrongType));

CollectionAssert 類別

驗證集合是否包含指定的項目
CollectionAssert.Contains(actual, element);
CollectionAssert.DoesNotContain(actual, element);

驗證集合的項目與順序是否相等
CollectionAssert.AreEqual(expected, actual);
CollectionAssert.AreNotEqual(expected, actual);

驗證集合的項目是否對等(不按順序)
CollectionAssert.AreEquivalent(expected, actual);
CollectionAssert.AreNotEquivalent(expected, actual);

CollectionAssert 類別

MSDN - CollectionAssert 類別 (Microsoft.VisualStudio.TestTools.UnitTesting)
https://msdn.microsoft.com/zh-tw/library/microsoft.visualstudio.testtools.unittesting.collectionassert.aspx

驗證第一個集合是否為第二個集合的子集
CollectionAssert.IsSubsetOf(subset, superset);
CollectionAssert.IsNotSubsetOf(subset, superset);

驗證集合中的所有項目都是唯一的
CollectionAssert.AllItemsAreUnique(actual);

 

如果要對回傳結果型別為 Dictionary<string, object> , Collection 或是 Custom 型別操作的話,以使用基本 Assert 語法操作來做驗證,就真的會考驗開發人員的耐心了。

於是在尋找並且評估並在專案裡實際使用,最後就決定使用 Fluent Assertions 這個套件做為 Assert 時的驗證輔助操作,因為就如同套件的名稱一樣,是可以在寫 Assert 時可以更加的流暢,而且是接近口語般的方式寫編寫 Assert 程式。

 

Fluent Assertions

http://www.fluentassertions.com/

image

https://github.com/dennisdoomen/fluentassertions

image

其實我覺得並不需要太多的介紹與使用說明,因為 Fluent Assertions 官網與 Github Wiki 的說明已經相當完整、清楚,

https://github.com/dennisdoomen/fluentassertions/wiki

image

不要只是光看 Wiki 裡說明而已,而是要照著做一遍,將範例內的程式逐一練習,幾次摸索與練習下來就會對使用 Fluent Assertions 相當上手。

 

Visual Studio 使用 Nuget 對單元測試專案加入 Fluent Assertions

不論是 VS2013 或 VS2015 都可以使用

NuGet Gallery | Fluent Assertions 4.0.1

https://www.nuget.org/packages/FluentAssertions/

image

image

以下的程式是 Fluent Assertions 在 Github Wiki 上的 Examples:

[TestClass()]
public class FluentAssertions_Sample
{
    [TestMethod]
    public void StringTest()
    {
        string actual = "ABCDEFGHI";
 
        actual.Should().StartWith("AB")
              .And.EndWith("HI")
              .And.Contain("EF")
              .And.HaveLength(9);
 
        string theString = "";
        theString.Should().NotBeNull();
 
        theString.Should().BeEmpty();
 
        theString.Should().HaveLength(0);
        theString.Should().BeNullOrWhiteSpace();
 
        theString = "This is a String";
        theString.Should().Be("This is a String");
        theString.Should().NotBe("This is another String");
        theString.Should().BeEquivalentTo("THIS IS A STRING");
 
        theString.Should().Contain("is a");
        theString.Should().NotContain("is aa");
        theString.Should().ContainEquivalentOf("THIS IS A STRING");
        theString.Should().NotContainEquivalentOf("HeRe ThE CaSiNg Is IgNoReD As WeLl");
 
        theString.Should().StartWith("This");
        theString.Should().NotStartWith("That");
        theString.Should().StartWithEquivalent("this");
        theString.Should().NotStartWithEquivalentOf("that");
 
        theString.Should().EndWith("a String");
        theString.Should().NotEndWith("a Numeric");
        theString.Should().EndWithEquivalent("a string");
        theString.Should().NotEndWithEquivalentOf("a numeric");
    }
 
    [TestMethod()]
    public void NullableTest()
    {
        short? theShort = null;
        theShort.Should().NotHaveValue();
 
        int? theInt = 3;
        theInt.Should().HaveValue();
 
        DateTime? theDate = null;
        theDate.Should().NotHaveValue();
    }
 
    [TestMethod()]
    public void BooleanTest()
    {
        bool otherBoolean = true;
 
        bool theBoolean = false;
        theBoolean.Should().BeFalse("it's set to false");
 
        theBoolean = true;
        theBoolean.Should().BeTrue();
        theBoolean.Should().Be(otherBoolean);
    }
 
    [TestMethod()]
    public void NumericTest()
    {
        int theInt = 5;
 
        theInt.Should().BeGreaterOrEqualTo(5);
        theInt.Should().BeGreaterOrEqualTo(3);
        theInt.Should().BeGreaterThan(4);
        theInt.Should().BeLessOrEqualTo(5);
        theInt.Should().BeLessThan(6);
        theInt.Should().BePositive();
        theInt.Should().Be(5);
        theInt.Should().NotBe(10);
        theInt.Should().BeInRange(1, 10);
 
        theInt = -8;
        theInt.Should().BeNegative();
 
        int? nullableInt = 3;
        nullableInt.Should().Be(3);
 
        double theDouble = 5.1;
        theDouble.Should().BeGreaterThan(5);
 
        byte theByte = 2;
        theByte.Should().Be(2);
    }
 
    [TestMethod()]
    public void DateTimeTest()
    {
        var theDatetime = new DateTime(2015, 3, 25, 13, 30, 0);
 
        theDatetime.Should().BeAfter(new DateTime(2015, 1, 1));
        theDatetime.Should().BeBefore(new DateTime(2016, 1, 1));
        theDatetime.Should().BeOnOrAfter(new DateTime(2015, 3, 25));
 
        theDatetime.Should().Be(DateTime.Parse("2015/3/25 13:30"));
        theDatetime.Should().NotBe(DateTime.Parse("2015/3/25"));
 
        theDatetime.Should().HaveDay(25);
        theDatetime.Should().HaveMonth(3);
        theDatetime.Should().HaveYear(2015);
        theDatetime.Should().HaveHour(13);
        theDatetime.Should().HaveMinute(30);
        theDatetime.Should().HaveSecond(0);
    }
 
    [TestMethod]
    public void CollectionsTest()
    {
        IEnumerable collection = new[] { 1, 2, 5, 8 };
 
        collection.Should().NotBeEmpty()
                  .And.HaveCount(4)
                  .And.ContainInOrder(new[] { 2, 5 })
                  .And.ContainItemsAssignableTo<int>();
 
        collection.Should().Equal(new List<int> { 1, 2, 5, 8 });
        collection.Should().Equal(1, 2, 5, 8);
        collection.Should().BeEquivalentTo(new int[] { 8, 2, 1, 5 });
        collection.Should().NotBeEquivalentTo(new int[] { 8, 2, 3, 5 });
 
        collection.Should().HaveCount(c => c > 3).And.OnlyHaveUniqueItems();
        collection.Should().HaveSameCount(new[] { 6, 2, 0, 5 });
 
        collection.Should().StartWith(1);
        collection.Should().EndWith(8);
 
        collection.Should().BeSubsetOf(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, });
 
        var otherCollection = new int[] { 1, 3, 5, 7 };
        collection.Should().IntersectWith(otherCollection);
 
        otherCollection = new int[] { 11, 13, 15 };
        collection.Should().NotIntersectWith(otherCollection);
 
        collection.Should().BeInAscendingOrder();
        collection.Should().NotBeDescendingInOrder();
 
        collection.Should().NotContain(82);
        collection.Should().NotContainNulls();
    }
 
    [TestMethod]
    public void DictionaryTest()
    {
        var dictionary1 = new Dictionary<int, string>
                          {
                              { 1, "One" },
                              { 2, "Two" }
                          };
 
        var dictionary2 = new Dictionary<int, string>
                          {
                              { 1, "One" },
                              { 2, "Two" }
                          };
 
        var dictionary3 = new Dictionary<int, string>
                          {
                              { 3, "Three" },
                          };
 
        dictionary1.Should().ContainKey(1);
        dictionary1.Should().NotContainKey(9);
 
        dictionary1.Should().ContainValue("One");
        dictionary1.Should().NotContainValue("Four");
 
        dictionary1.Should().Equal(dictionary2);
        dictionary1.Should().NotEqual(dictionary3);
    }
}

 

下面驗證 Collection 資料的兩種 Assert 方式,第一種是使用 MsTest 所提供的 CollectionAssert 類別

[TestClass]
public class CollectionAssertSample
{
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內是否包含特定值()
    {
        var item = 1;
        var actual = new int[] { 1, 3, 5, 7, 9 };
 
        CollectionAssert.Contains(actual, item);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內是否不包含特定值()
    {
        var item = 2;
        var actual = new int[] { 1, 3, 5, 7, 9 };
 
        CollectionAssert.DoesNotContain(actual, item);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內按照順序_每個項目是否相等()
    {
        var expected = new int[] { 1, 3, 5 };
        var actual = new int[] { 1, 3, 5 };
 
        CollectionAssert.AreEqual(expected, actual);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內按照順序_每個項目是否不相等()
    {
        var expected = new int[] { 1, 3, 5, 7 };
        var actual = new int[] { 7, 5, 3, 1 };
 
        CollectionAssert.AreNotEqual(expected, actual);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內容是否對等()
    {
        // 兩個集合個數一樣,順序不一樣
        var expected = new int[] { 1, 3, 5, 7 };
        var actual = new int[] { 7, 5, 3, 1 };
 
        CollectionAssert.AreEquivalent(expected, actual);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內容是否不對等()
    {
        var expected = new int[] { 2, 4, 6 };
        var actual = new int[] { 1, 3, 5 };
 
        CollectionAssert.AreNotEquivalent(expected, actual);
    }
 
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_是否為子集合()
    {
        var superset = new int[] { 1, 3, 5 };
        var subset = new int[] { 5, 3 };
 
        CollectionAssert.IsSubsetOf(subset, superset);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_是否不為子集合()
    {
        var superset = new int[] { 1, 3, 5 };
        var subset = new int[] { 2, 3 };
 
        CollectionAssert.IsNotSubsetOf(subset, superset);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內項目是否唯一()
    {
        //var actual = new int[] {1, 1, 3, 5};
        var actual = new int[] { 1, 3, 5 };
 
        CollectionAssert.AllItemsAreUnique(actual);
    }
}

接著下面則是改用 Fluent Assertions 的方式做 Collection 資料的驗證,可以將兩種方式做個比較,就可以知道差別了,另外還添加了幾種驗證方式讓大家可以更加瞭解如何用 Fluent Assertions 對 Collection 資料做驗證,

[TestClass()]
public class CollectionAssertSample_FluentAssertions
{
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內是否包含特定值()
    {
        var item = 1;
        var actual = new int[] { 1, 3, 5, 7, 9 };
 
        actual.Should().Contain(item);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內是否不包含特定值()
    {
        var item = 2;
        var actual = new int[] { 1, 3, 5, 7, 9 };
 
        actual.Should().NotContain(item);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內按照順序_每個項目是否相等()
    {
        var expected = new int[] { 1, 3, 5 };
        var actual = new int[] { 1, 3, 5 };
 
        actual.Should().Equal(expected);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內按照順序_每個項目是否不相等()
    {
        var expected = new int[] { 1, 3, 5, 7 };
        var actual = new int[] { 7, 5, 3, 1 };
 
        actual.Should().NotEqual(expected);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內容是否對等()
    {
        // 兩個集合個數一樣,順序不一樣
        var expected = new int[] { 1, 3, 5, 7 };
        var actual = new int[] { 7, 5, 3, 1 };
 
        actual.Should().BeEquivalentTo(expected);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內容是否不對等()
    {
        var expected = new int[] { 2, 4, 6 };
        var actual = new int[] { 1, 3, 5 };
 
        actual.Should().NotBeEquivalentTo(expected);
    }
 
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_是否為子集合()
    {
        var superset = new int[] { 1, 3, 5 };
        var subset = new int[] { 5, 3 };
 
        subset.Should().BeSubsetOf(superset);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_是否不為子集合()
    {
        var superset = new int[] { 1, 3, 5 };
        var subset = new int[] { 2, 3 };
 
        subset.Should().NotBeSubsetOf(superset);
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    [TestCategory("CollectionAssertSample")]
    public void 驗證Collection_集合內項目是否唯一()
    {
        //var actual = new int[] {1, 1, 3, 5};
        var actual = new int[] { 1, 3, 5 };
 
        actual.Should().OnlyHaveUniqueItems();
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    public void 驗證Collection_集合內項目是否為Ascending排序()
    {
        var actual = new int[] { 1, 3, 5, 6, 7, 8, 9 };
 
        actual.Should().BeInAscendingOrder();
        actual.Should().NotBeDescendingInOrder();
    }
 
    [TestMethod()]
    [Owner("User_Name")]
    public void 驗證Collection_集合內項目是否為Descending排序()
    {
        var actual = new int[] { 10, 8, 5, 4, 3, 2, 1 };
 
        actual.Should().BeInDescendingOrder();
        actual.Should().NotBeAscendingInOrder();
    }
 
    [TestMethod()]
    public void 驗證Collection_集合項目的數量是否如預期()
    {
        var actual = new int[] { 1, 2, 3, 4, 5 };
 
        actual.Should().HaveCount(5);
    }
 
    [TestMethod]
    public void 驗證Collecion_兩個集合項目的數量是否相同()
    {
        var actual = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
        var expected = new int[] { 2, 4, 6, 8, 10, 12, 14, 16 };
 
        actual.Should().HaveSameCount(expected);
    }
}

 

使用 Fluent Assertions 處理 Exception 的驗證

其實這個應用的操作示範在之前的文章「NSubstitute 練習題 - 拋出 Exception」裡就可以說明過了,一般如果是使用 MsTest 要對 Exception 的驗證就要使用 ExpectedExceptionAttribute 這個類別,雖然這樣的操作並沒有什麼不妥,但對於習慣 3A 原則的方式,對於 Exception 的驗證要跳脫 3A 原則就會不習慣與不那麼直覺。

而使用 Fluent Assertions 所提供的方法對於 Excepton 的驗證就可以用符合 3A 原則的做法來完成,先看看 Fluent Assertions 官網文件的範例,

image

 

使用 MsTest 的 ExpectedExceptionAttribute 類別

image

改用 Fluent Assertions 對拋出 Exception 做驗證,

image

 

對於單元測試的初學者而言,建議還是要先熟悉 MsTest 或是其他 Testing Framework 的 Assert 操作方法,等到對於單元測試的程式編寫已經熟悉並且建立觀念之後,再來考慮改用 Fluent Assertions。

 


到現在為止,我在公司所分享的內容還不及「自動測試與 TDD 實務開發(使用C#)」課程內容的一半,要一次講完也是可以,但同事們並無法在短期間裡完全吸收並且就可以直接應用在實務開發上,所以就必須要依據團隊每位成員現況、程度去對課程內容做調整,因為在公司的分享並不是自己講爽的,也不是要突顯自己有多厲害,而是要讓每位同事都能夠在實務開發上都能夠用到,而且解決他們長久以來在開發上所遇到的問題。

我秉持著一個想法,「自己厲害並不是真的厲害,如果能夠讓你周遭的人藉由你的分享而擁有與你一樣的技術,那才是真的厲害」,我不喜歡藏招,藏招是沒用的,因為藏招只會讓自己故步自封,進步的速度會相當地緩慢,而且團隊並不會因為你的藏招而有所進步。

我在公司試歡迎大家來找我問問題或是一起討論,我並不是什麼都知道,技術也不是最強,但藉由一起解決問題、共同討論的時候讓團隊成員彼此之間的資訊與技術落差能夠逐漸消弭與一同提升,很多想法與做法並不是閉門造車就可以完成的,還是需要團隊一起合作完成的,所以資訊的分享與共享是絕對必要的。

前陣子被一個盜文的人給搞得有點意興闌珊,還被這個人說我自私,如果我自私的話,我又何必在這個部落裡一字一字的去寫了四百多篇的文章呢?而且每一篇文章都不是那種「轉貼文」。如果我自私的話,我又何必花時間去做範例、將範例放到 Github 呢?心灰意冷了幾天後,想了想,我又何必在意一個還會在 HTML 裡直接指定網頁字型使用「微軟正黑體」的人所說的話,就不管他了。

 

如果你還不知道 91 哥所講的「自動測試與 TDD 實務開發(使用C#)」課程有多威,那麼我建議你先看看 twMVC #12 這一場由 91 哥所主講的「如何在實務上使用 TDD 來開發」課程錄影(不是片段,而是整場課程內容)

https://www.youtube.com/watch?v=dZ_uZmoO2Aw

image

http://mvc.tw/Event/2013/11/30

https://docs.com/is-twMVC/4329/tdd-twmvc-12

如果你以為 91 哥在 SkillTree 所講授的「自動測試與 TDD 實務開發(使用C#)」課程內容只是 twMVC#12 這一場的加長版,那麼你就大錯特錯了,因為開課的四個梯次,每個梯次都是場場爆滿,而且課程結束後的學員反應都是好評不斷,別以為只有 .NET C# 的開發者來上課,以第三、第四梯次的學員觀察,有將近一半的學員在工作上所使用的開發技術都不是 .NET 陣營,所以觀念與心法在不管什麼樣的技術都是相同的。91 哥是將他本身專案的經驗與技術全部傳授給學員,不是虛晃幾招,每招每式都是拳拳到肉,每一拳都是紮實並打到你的心裡。

想瞭解 91 哥的課程資訊、資訊分享以及課後學員心得感想,請不要錯過「91 敏捷開發之路」臉書粉絲專頁,另外不想錯過 91 哥的課程以及其他專頁技術課程,請務必關注「SkillTree.My」的臉書專頁官網


P.S.
如果你是我的同事,我建議你在公司可以等我的分享或是直接問我、與我討論,等到下次 91 哥有開課之後就趕快去報名,這樣你就會更有感。

 

相關連結

http://www.fluentassertions.com/

https://github.com/dennisdoomen/fluentassertions

https://github.com/dennisdoomen/fluentassertions/wiki

https://www.nuget.org/packages/FluentAssertions/

點部落 - In 91

91 敏捷開發之路

SkillTree 官網

SkillTree 臉書粉絲專頁

 

以上

2 則留言:

  1. Are you looking for free Twitter Re-tweets?
    Did you know that you can get these AUTOMATICALLY AND TOTALLY FREE by using Like 4 Like?

    回覆刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。

最近的留言