1. 程式人生 > >.net持續整合測試篇之Nunit 測試配置

.net持續整合測試篇之Nunit 測試配置

系列目錄

在開始之前我們先看一個陷阱

用到的Person類如下

 public class Person:IPerson
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public DateTime BirthDay { get; set; }
        /// <summary>
        /// 判斷Name是否包含字母B
        /// </summary>
        /// <returns></returns>
        public bool WhetherNameContainsB()
        {
            if (this.Name == null) throw new ArgumentNullException("引數不能為null");
            if (this.Name.Contains("B")) return true;
            return false;
        }
    }

這個類以前也用過,有三個屬性和一個方法,其中方法用於判斷Name欄位是否包含大寫字母B,如果包含返回true,不包含返回false,如果Name為null則丟擲異常

測試類如下

   [TestFixture]
    public class FirstUnitTest
    {
        private Person psn;
        public FirstUnitTest()
        {
         psn = new Person();
         }

        [Test]
        [Order(1)]
        public void SetPersonName()
        {
            psn.Name = "sto";
            Assert.IsNotEmpty(psn.Name);
        }
        [Test]
        [Order(2)]
        public void DemoTest()
        {
            Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB());
        }
       

    }

第一個測試給Name賦值,然後斷言使用者名稱不為空,這顯然應該是通過的

第二個測試用於斷言呼叫WhetherNameContainsB時會拋異常,由於這裡Name並沒有賦值,所以會丟擲異常,這裡也應該能返回成功.

然而執行以上程式碼第二個測試返回的是失敗!這是因為Nunit在執行測試類的時候會呼叫所有的測試方法,由於我們顯式指定的執行順序(使用order註解)則第一個方法先於第二個方法前執行,由於第一個方法把Name設定為"sto",因此這時候全域性psn的Name欄位便有值了.所以第二個方法再呼叫psn的WhetherNameContainsB方法時,是不會丟擲異常的(方法的邏輯是隻有Name有值便不會丟擲異常).

如果不指定執行順序,則第二個方法執行的結果是不確定的,如果它先於第一個方法執行,則就會返回成功,如果晚於第一個方法則返回失敗.

我們前面說到,單元測試的結果應該是穩定的,然而這裡卻是不確定的,因此我們要重新設計.

當然其實解決這個問題很簡單,只要把對全域性的變數移動到方法裡面就行了,這樣每個方法的狀態就不會被外部改變了.

改造後的測試類如下

 [TestFixture]
    public class FirstUnitTest
    {
       
        public FirstUnitTest()
        {
        
         }

        [Test]
        [Order(1)]
        public void SetPersonName()
        {
            Person psn = new Person();
            psn.Name = "sto";
            Assert.IsNotEmpty(psn.Name);
        }
        [Test]
        [Order(2)]
        public void DemoTest()
        {
            Person psn = new Person();
            Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB());
        }
       

    }

我們再執行,便都能通過了.

然而這樣設計有一個問題,第一如果多個測試方法都要用到這個物件,則需要複製很多,第二如果多個方法之間共用的程式碼非常多,那麼每個方法裡都要複製很多程式碼,我們前面說過單元測試裡的程式碼應力求簡潔明瞭,並且複製同樣的程式碼不利於維護.下面我們介紹Nunit裡的Setup

Setup註釋

在單元測試類中如果把一個方法加上setup註解,則這個方法會先於其它未標的方法執行,並且每個方法執行之前都會執行它,如果在setup註解的方法內初始化物件,則每個方法執行之前都會執行這個被註解的方法,則每次變數都重新初始化,不會再有資料被共享造成的各種問題了.我們用setup改造後的測試類如下

    [TestFixture]
    public class FirstUnitTest
    {
        private Person psn;
        public FirstUnitTest()
        {
        
         }

        [SetUp]
        public void Setup()
        {
            psn = new Person();
        }
        [Test]
        [Order(1)]
        public void SetPersonName()
        {
          
            psn.Name = "sto";
            Assert.IsNotEmpty(psn.Name);
        }
        
        [Test]
        [Order(2)]
        public void DemoTest()
        {
           
            Assert.Throws<ArgumentNullException>(() => psn.WhetherNameContainsB());
        }
       

    }

我們在標識為Setup的方法裡初始化Person,這樣測試就能通過了

被Setup註解的方法名可任意取,只要符合命名規範即可

Nunit並不限制一個測試類中有多個Setup方法,但是強烈不建議這麼做.

OneTimeSetup註釋

OneTimeSetup也是在所有的測試方法執行之前執行,不同的是它並不像SetUp一樣每個測試方法執行之前都會執行,而是在所有測試方法執行之前之執行一次.它適用這樣場景:比如說我們程式裡的資料訪問封閉類,這個類裡面一般都是訪問資料庫的各種方法和一些私有的變數像連線字串之類的,資料訪問方法裡只會去讀取這些欄位而不去修改它.最為重要的是每個測試方法執行之前都去實體化一個這樣的類會很耗費資源.像這種型別便可以放在OneTimeSetup方法裡,在類建立的時候執行一次.

這個方法功能很像建構函式,它能做的工作一般建構函式也能做.

Teardown

Teardown和Setup用法一樣,只是它是在測試方法執行之後才執行,如果我們的測試方法裡有需要釋放的物件可以在這個方法裡釋放.

OneTimeTearDown

它是在所有的方法都執行完之後才執行一次,功能上相當於解構函式,用於在測試類所有方法都執行完以後釋放掉類中使用的資源.

前面部分我們講了如何在所在單元測試執行之前以及在每一個單元測試之前如何執行一個特定的方法.下面講解如何在程式集執行之前和執行之後執行某一指定方法.

可能會有人懷疑這樣做的意義,的確,大部分時候我們可能不需要在程式集執行之前或者之後執行某一方法,但是特定的情況下這樣做確實會給測試帶來很大幫助.比如以下場景

  • 我們想要統計一下所有測試方法的執行時間,這時候我們可以在程式集之前啟動StopWatch並在所有方法執行完之後獲得執行時間,並寫入日誌.當然這樣做可能顯得有點傻.
  • 在Web專案中可能會大量使用ConfigurationManager.AppSetting[xxx]來獲取web專案配置,這樣做給測試帶來難題

    由於單元測試的執行環境很多時候並非在程式的輸出目錄,因此web專案使用到AppSetting配置的方法在web環境執行正常,但是在單元測試環境得到的值都是Null,這將會導致測試時大量業務覆蓋不到.

    在測試的時候我們很難通過傳參來改變這個值,因為在程式中往往都是獲取AppSetting裡的值,而不是設定,因此它往往不包含在方法的引數裡.也就沒法通過傳參來修改它.

    我們如果在Setup裡給AppSetting賦值,比如ConfigurationManager.AppSettings["user"] = "sto";這樣在執行的時候我們便可以獲取到這個值了,但是AppSetting是全域性的,可能程式中很多方法都用到了它,我們在每個測試方法裡都寫個Setup方法給它複製顯然非常boring.

    這時候我們可以在程式集執行之前執行一個方法,在這個方法裡給AppSetting賦值,這樣測試方法執行的時候使用到AppSetting的地方就可以獲取到值了.

    要做到這一點,我們需要新建一個類,並把類上加上SetUpFixture註解.然後方法上加上OneTimeSetUp和OneTimeTeardown註解.這樣Nunit就會在程式集載入的時候掃描到這個類,然後對它處理.

    我們看一下示例程式碼

    [SetUpFixture]
    public  class AssemblySetup
      {
          [OneTimeSetUp]
          public void RunBeforeEveryMethod()
          {
              ConfigurationManager.AppSettings["user"] = "sto";
              ConfigurationManager.AppSettings["age"] = "32";
          }
      }

    我們新建這個類以後RunBeforeEveryMethod便會在程式集中所有程式碼執行之前運行了

我們看執行結果

我們可以看到,在測試類中隨便找一個方法裡面去獲取值,都可以獲取到了.

前面我們講解了如何在方法執行前後,在測試類的所有方法執行前後以及如何在程式集,下面我們講一下如何自定義一個方法在測試方法執行之前/之後執行.

自定義方法的優勢在於如果每個測試類的setup裡執行的程式碼基本相同,只是稍微有一點差異,這樣就會導致程式碼重複的問題.比如我們要在方法執行之前和之後記錄一些日誌,這樣我們就可以自定義一個方法實現在測試方法執行前後執行這個自定義方法,減少程式碼重複.

要實現自定義執行方法,我們要繼承TestactionAttribute
示例程式碼如下

public class MyTestAction:TestActionAttribute
    {
        public override void BeforeTest(ITest test)
        {
            Console.WriteLine("★★★★★★★★★★" + test.FullName);
        }
       
    }

我們用Console.WriteLine模擬.

Itest物件由Nunit在執行時注入.

然後我們要在執行這個自定義方法的類上加上MyTestAction註解即可.

自定義執行方法非常強大,還可以提供引數,這樣會在大幅度減少相似程式碼的重複,提高可維護性,大家要以後的測試中慢慢體