.net持續整合測試篇之Nunit引數化測試

周國通發表於2019-08-07

系列目錄

在進行單元測試的時候,很多時候,很多時候我們都是在單元測試方法內部提供特定的值,但是這樣測試往往造成樣本數不足從而導致覆蓋的結果不夠全面,很多時候我們更想提供來自外部的,滿足條件的一組值來進行測試.其實Nunit框架本身提供了為測試用例提供值的能力.我們可以對它進行擴充套件來實現匯入外部的值來填充到測試方法內部.很多朋友也自己寫了不少按照一定規則生成值的方法.但是往往都是在方法內部直接呼叫,這樣就會和單元測試的邏輯混雜在一塊,導致測試方法本身不夠簡潔.其實可以根本測試框架本身的能力改造成為註解的方式,這樣引數生成邏輯和測試邏輯一目瞭然.後面我們還會講解基於Autofixture框架來生成填充資料,autofixture相比我們自己寫的值填充方法,往往功能更加強大.後面我們將見證其強大之處.

提供普通引數

很容易發現,單元測試的方法都是不帶引數的,有些時候我們需要為一個要測試的方法(並非單元測試方法)提供多個引數進行測試,這就會導致一個問題:我們需要寫很多類似的測試方法,只是引數不一樣,這樣維護起來不方便,同時大量重複的工作也很煩.下面介紹Nunit裡如何為測試提供引數


        int  Add(int x, int y)
        {
            return x + y;
        }

以上是我們要測試的方法.

雖然Nunit測試方法正常情況下是不支援引數的,但是如果對引數新增的values註解,Nunit便會把這些引數應用到測試.
我們看一下編寫的測試方法

        [Test]
        public void DemoTest([Values(3,4,5)]int a,[Values(6,7,8)]int b)
        {
            var result = Add(a, b);
            Assert.AreEqual(a + b, result);
        }

我們執行以上方法,可以看到測試結果通過,但是我們看一下測試皮膚(Test Explorer)

avatar
通過截圖我們很容易發現,這個測試方法一共執行的九次!再仔細看看方法對應的引數,可以看到它是使用組合的方式把所有的可能都組合一遍.

但是有些時候我們想要的不是這樣的組合,我們想要的更多時候是(3,6),(4,7),(5,8)這樣的組合,如何做到呢,仍然看一段示例程式碼

        [Test]
        [Sequential]
        public void DemoTest([Values(3,4,5)]int a,[Values(6,7,8)]int b)
        {
            var result = Add(a, b);
            Assert.AreEqual(a + b, result);
        }

我們看看執行結果

avatar

這次只執行了三次,並且引數的組合正如我們期待的.
這個方法和上面的一樣,只是多了一個[Sequential]註解

注意Values註解裡的引數都是Object型別,執行時候轉換為引數的真正型別,如果無法轉換則會丟擲異常.比如[Values("a")]int x由於a是字串型別,通過內建方法無法轉換為int,因些會丟擲異常.

提供基於範圍的引數

上面的測試Values(3,4,5)和Values(6,7,8)都是連續的數字,如果連線的引數更多,我們可以使用基於範圍的引數.

看以下示例程式碼

        [Test]
        [Sequential]
        public void DemoTest([Range(3,5)]int a,[Range(6,8)]int b)
        {
            var result = Add(a, b);
            Assert.AreEqual(a + b, result);
        }

我們把Values註解改為Range註解,就ok了

提供隨機引數

我們還可以為測試提供一些隨機數,以使測試變得更隨機,覆蓋範圍更大

這裡要使用Random註解
請看下面示例

       [Test]
       [Sequential]
        public void DemoTest([Random(3)]int a, [Random(3)]int b)
        {
            var result = Add(a, b);
            Assert.AreEqual(a + b, result);
        }

Random的引數為要生成隨機數的個數.

Random還有一過載以支援生成隨機數的最大值和最小值

       [Test]
       [Sequential]
        public void DemoTest([Random(3,10,2)]int a, [Random(5,9,3)]int b)
        {
            var result = Add(a, b);
            Assert.AreEqual(a + b, result);
        }

示例中Random的三個引數分別是最小值,最大值和個數

[info]Random的最大值和最小值不僅可以是整數,也可以是小數

提供計算引數

先看一個示例

       [Test]
       [Sequential]
        public void DemoTest(DateTime dt1)
       {
           DateTime dt2 = default(DateTime);
           Assert.Greater(dt1, dt2);
       }

這裡測試方法的引數是Datetime型別,我們如何給給它提供值呢,很多人可能會想使用Values[DateTime.Now] 來註解dt1引數,然而不幸的是Values註解只接受const型別的值,這裡介紹ValueSource註解來解決這個問題.

ValueSource的機制是使用一個方法來獲取值,然後提供給測試方法引數,它接受一個字串型別的引數,用於指定提供值的方法名.

我們用以下方法生成一些DateTime值

static IEnumerable<DateTime> GetPeople()
        {
            yield return DateTime.Now;
            yield return DateTime.Now.AddDays(2);
        }

以上方法生成了一個包含兩個DateTime值的集合.下面我們看如何使用它

       [Test]
        public void DemoTest([ValueSource(nameof(FirstUnitTest.GetPeople))]DateTime dt1)
       {
           DateTime dt2 = default(DateTime);
           Assert.Greater(dt1, dt2);
       }

我們使用nameof獲取剛才生成的用於提供值的方法,作為ValueSource的引數.

使用nameof而不是使用手寫字串的好處在於nameof可以有智慧提示,防止手寫出現錯誤,另外就是如果方法名更改,這裡將會丟擲了一個錯誤,靜態字串不會提示錯誤,如果在執行時找不到這個方法則會丟擲執行時錯誤

用於為ValueSource提供值的方法必須是靜態的

以上程式碼,我們把提供值的方法直接寫在測試類裡,這並不是一種很好的實踐,一種好的做法是把所有的用於提供值的方法放在一個外部的類中.

我們把這個類移動到一個叫作MyValueProvider的類中
程式碼如下

public class MyValueProvider
    {
        public static IEnumerable<DateTime> GetPeople()
        {
            yield return DateTime.Now;
            yield return DateTime.Now.AddDays(2);
        }
    }

單元測試方法改成如下:

       [Test]
        public void DemoTest([ValueSource(typeof(MyValueProvider),nameof(MyValueProvider.GetPeople))]DateTime dt1)
       {
           DateTime dt2 = default(DateTime);
           Assert.Greater(dt1, dt2);
       }

如果把值提供方法不在本類中(當前測試方法所在的類),提供一個Type型別(提供值的方法所在的類的型別)作為第一個引數,方法名作為第二個引數.

上面講的都是基於引數註解的值提供方法,這裡基於方法的註解的值提供方法.當然,它完成的功能基於引數註解的方法也同樣能完成.

TestCaseAttribute註解

看以下程式碼片段

       [TestCase(3,4)]
        public void DemoTest(int x,int y)
       {
           var val = Add(x, y);
           Assert.AreEqual(x + y, val);
       }

其中用到的Add方法程式碼如下

 int  Add(int x, int y)
        {
            return x + y;
        }

TestCase的工作原理是這樣的,它提供的值是基於位置的,每一個位置處的值賦值給第一個引數,第二個位置處的值提供給第二個引數...

有了TestCase註解之後,Test註解不再是必要的.

TestCaseSourceAttribute註解

從上ValueSource我們很容易想到可能會有TestCaseSource,實際上也確實是這樣的,TestCaseSource功能也同ValueSource一樣,用於提供基於計算的結果.

用於提供值的類如下

 public class MyValueProvider
   {
       public static ArrayList ar = new ArrayList
       {
           new int[] {3, 4},
           new int[] {5, 9},
           new int[] {9, 22}
       };
   }

測試方法如下

       [TestCaseSource(typeof(MyValueProvider),nameof(MyValueProvider.ar))]     
        public void DemoTest(int x,int y)
       {
           var val = Add(x, y);
           Assert.AreEqual(x + y, val);
       }

從這個例子我們可看到,不僅方法可以提供值,屬性,普通欄位也可以提供值

為TestCaseSource提供值的欄位,方法,屬性也必須是靜態的

TestCase和TestCaseSource都支援多重註解,有幾個註解,測試方法就會執行幾次.

相關文章