為什麼會需要做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下載ExpectedObjectsvar 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下載FluentAssertionAction 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的偵錯