TDD 筆記(0)

為什麼會需要做Unit Test ? 寫Code都來不及了哪有時間寫測試? 我的程式碼很難做測試? 

Unit Test 是很多前輩走過的路留下的經驗所產生的產物,在還沒有接觸測試以前,往往使用單步執行測試程式,必須讓程式跑個一大段才會走到要測試的Code,實在很沒有生產效率,或沒有架設環境,程式沒辦法模擬更不好除錯,又或著當需求改變,改Code牽一髮動全身的窘境,當一隻程式越寫越大這些問題就越不好掌控。

 

做Unit Test 需要掌握幾個大方向,每次測試只測試最小功能、單一個Function,而且盡可能不因環境有所關聯,而是很單純的一件事情,所以不要包含外部API、資料庫、檔案系統、外部硬體等等,也不會有任何邏輯判斷,也就是說只測試這件事做得對還是不對。正因如此每一個測試項目所花時間都很短,請不要超過500ms,不因環境有所關聯,移植到不同環境測試、不論重複執行幾次測試所得到的結果都必須相同,而且要很容易因這個測試項目的結果,明確很指出環節的癥結點,而不因寫測試有壓力,寫Code邊寫測試,測試寫完剛好Product Code也一起寫完。

 

Unit Test的流程:

建立測試的物件,並模擬其物件行為,設定期望的結果,實際呼叫物件的方法,檢查其結果是否與期望值相同。

 

說了這麼多你可能會問我Unit Test究竟在測甚麼東西?

總括三點,驗證Method的結果是否如預期、驗證物件是否如預期地改變狀態、驗證物件是否如預期與其他物件進行互動。

 


以Visual Studio 內建的測試框架MSTest來說,第一個要認識TestClass常用的Attribute,一個TestClass 的生命週期,從AssemblyInitialize > { ClassInitialize  > [ TestInitialize > TestMethod > TestCleanup ] > ClassCleanup } > AssemblyCleanup,中間用[]刮起來的部分,每一則TestMethod都會執行的部分,用{}刮起來的部分則是每一個TestClass都會行的部分。

    // 測試的Class
    [TestClass]
    public class TheTestClassTest
    {
        // 所有TestClass執行前,使用 AssemblyInitialize 執行程式碼
        [AssemblyInitialize()]
        public static void MyAssemblyInitialize(TestContext context) { }

        // 執行該類別中第一項測試前,使用 ClassInitialize 執行程式碼
        [ClassInitialize()]
        public static void MyClassInitialize(TestContext testContext) { }
        
        // 在執行每一項測試之前,先使用 TestInitialize 執行程式碼
        [TestInitialize()]
        public void MyTestInitialize() { }
        
        // 一項測試
        [TestMethod()]
        public void TheTestMethod() { }

        // 在執行每一項測試之後,使用 TestCleanup 執行程式碼
        [TestCleanup()]
        public void MyTestCleanup() { }

        // 在類別中的所有測試執行後,使用 ClassCleanup 執行程式碼
        [ClassCleanup()]
        public static void MyClassCleanup() { }

        // 所有TestClass執行完畢,使用 AssemblyCleanup執行程式碼
        [AssemblyCleanup()]
        public static void MyAssemblyCleanup() { }  

    }

 

再來是可以在TestMethod上面加掛的Attribute有:

  • [TestCategory("assert exception")]  // 幫TestMethod分類,顯示於測試總管
  • [Ignore]  // 執行測試時忽略此TestMethod

 

MSTest使用的驗證方式有:

  • Assert:基本驗證
  • CollectionAssert:集合驗證
  • ExpectedException:例外驗證

但是整體來說簡單的驗證Assert已經足夠,CollectionAssert的集合驗證,無法明確的比較兩個集合差異性(不會說明不一樣的地方在哪),也無法直接比較兩個物件的各個屬性是否相等,ExpectedException則是用Attribute掛在TestMethod上,看看此TestMethod是否會如預期拋出指定的Exception,但是這樣就不知道是在哪一行拋出的,有可能其中有兩行都有可能,無法明確地指出錯誤點。MSTest沒有提供Mock的東西讓我們驗證是否有與其他物件互動的行為。

 

重新整理建議使用的驗證方式:

  • Assert:基本驗證
  • CollectionAssert:集合驗證  =>  請從Nuget下載ExpectedObjects 
    var expected = new List<Person>
    {
    	new Person { Id=1, Name="A",Age=10},
    	new Person { Id=2, Name="B",Age=20},
    	new Person { Id=3, Name="C",Age=30},
    };
    
    var actual = new List<Person>
    {
    	new Person { Id=1, Name="A",Age=10},
    	new Person { Id=2, Name="B",Age=20},
    	new Person { Id=3, Name="C",Age=30},
    };
    
    expected.ToExpectedObject().ShouldEqual(actual);

     

  • ExpectedException:例外驗證  =>  請從Nuget下載FluentAssertion 
    Action act = () => MethodName(a, b);
    // 監控此Method 是否會拋出IOException
    act.ShouldThrow<IOException>();

 

  • NSubstitute :Mock假物件  =>  請從Nuget下載NSubstitute 
    // 建立其互動的假物件
    var log = Substitute.For<ILog>();
    
    // 檢查Save方法被呼叫時,傳入參數是否包含"login failed",並被呼叫過一次
    log.Received(1).Save(Arg.Is(x => x.Contains("login failed")));

     

 

建議測試的項目名稱可以一目了然,最好看到Method的名稱就知道測試項目,而且不應包括任何邏輯、if-else、switch-case、for,如以下範例:

[TestMethod()]
public void AddTest_first_is_1_second_is_2_should_be_3()
{
        //arrange
        var target = new MyCalculator();
        var first = 1;
        var second = 2;
        var expected = 3;
            
        //act
        var actual = target.Add(first, second);

        //assert
        Assert.AreEqual(expected, actual);
}

 

善用快捷鍵:

  • 「Ctrl + R, T」單一TestMethod的執行
  • 「Ctrl + R, A」全部TestMethod的執行
  • 「Ctrl + R,  Ctrl + T」單一TestMethod的偵錯
  • 「Ctrl + R,  Ctrl + A」全部TestMethod的偵錯