.net持續集成測試篇之Nunit參數化測試

  • 2019 年 10 月 3 日
  • 筆記

系列目錄

在進行單元測試的時候,很多時候,很多時候我們都是在單元測試方法內部提供特定的值,但是這樣測試往往造成樣本數不足從而導致覆蓋的結果不夠全面,很多時候我們更想提供來自外部的,滿足條件的一組值來進行測試.其實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都支援多重註解,有幾個註解,測試方法就會運行幾次.