使用 MSTest 撰寫單元測試套路整理 (1) - 基本技巧

前言

Visual Studio 內建的 MSTest 測試框架(test framework),是目前我在工作上使用的主力。當然相較社群愛用的 xUnitNUnit 來說,它的功能上有點過於陽春,但搭配一些好用的社群 library,它仍然可以完成我工作上大部份的需求。

最近因為部門有許多新人來報到,有許多關於測試上的 FAQ。被問多了,讓我產生整理 MsTest 在撰寫單元測試上一些方式的想法。當然這些都是套路,只是程式碼,實務上究竟要怎麼利用這些技巧,還是要依照專案的情境做調整。這才是最考驗工程師能力的地方,也是未來可以自我修養的部份。

目錄

1. 基本測試結構

下面是個簡單的加法計算機類別 Calculator,方法 Add() 接受 2 個參數 number1number2,並回傳這 2 個數字的加總結果。

Production Code (Calculator)

1
2
3
4
5
6
7
public class Calculator
{
public int Add(int number1, int number2)
{
return number1 + number2;
}
}

這時候我們可以看看,要如何測試 Add() 方法是否正確?下面是基本的測試程式碼︰

Test Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[TestClass]
public class CalculatorTest
{
[TestMethod]
public void Add_3加4_回傳值應為7()
{
// Arrange
var target = new Calculator();

// Act
var actual = target.Add(3, 4);

// Assert
var expected = 7;
Assert.AreEqual(expected, actual);
}
}

可以看到測試程式,由 3 個段落組成︰

1. Arrange

建立、安排測試環境。

本例中我們是建立待測類別 Calculator 的物件,慣例上我們會以 targetsut (subject under test) 變數來表達待測物件。

2. Act

呼叫待測類別上,我們想測試的目標方法。

本例中我們是呼叫 Add() 方法,慣例上我們會以 actual 變數表達待測物件的回傳值,或是物件的現實狀態。

3. Assert

驗証待測物件的行為是否符合預期。

本例中我們是驗証 Add() 的回傳值,是否如同我們預期,慣例上我們以 expected 表達我們預期的行為值,一般會以常數表達。

回到目錄

2. 驗証是否拋出例外

錯誤處理是開發程式的基本議題,我們會適當的以 Exception 例外表達錯誤狀況,並附上夠清楚的 debug 相關資訊以利除錯。

比方說前例中的 Calculator 類別中的 Add() 方法,PO 要求參數「不得為負值」,因此我們會將 production code 改為以下內容︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Calculator
{
public int Add(int number1, int number2)
{
if (number1 < 0)
{
throw new ArgumentException("參數 number1 應為大於等於 0 的正整數");
}

if (number2 < 0)
{
throw new ArgumentException("參數 number2 應為大於等於 0 的正整數");
}

return number1 + number2;
}
}

針對這樣的 production code,該如何撰寫測試程式,確保程式拋出的例外資訊,真的如同我們所預期的呢?

回到目錄

2.1 驗証是否拋出例外 - 基本作法

根據目前的理解,我們可能會將在 基本測試結構 學到的測試程式寫法,改為以下的內容︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[TestClass]
public class CalculatorTest
{
[TestMethod]
public void Add_第一個數值為負數_應拋出例外()
{
// Arrange
var target = new Calculator();

var exceptionIsThrown = false;
try
{
// Act
target.Add(-3, 4);
}
catch (ArgumentException e)
{
exceptionIsThrown = true;

// Assert
var actual = e.Message;
var expected = "參數 number1 應為大於等於 0 的正整數";

Assert.AreEqual(expected, actual);
}

// 驗証 exception 真有拋出來
Assert.IsTrue(exceptionIsThrown);
}
}

上述程式看似可行,但有一些明顯的缺點︰

  1. Arrange-Act-Assert 的結構被破壞,程式可讀性不見了。
  2. 為了驗証 Add() 真的有拋出我們預期的 Exception,我們還要使用一個 exceptionIsThrown 的變數,記錄例外有被丟出來。

有沒有更好的做法呢?

回到目錄

2.2 驗証是否拋出例外 - 使用 MsTest 內建作法

其實 MsTest 有內建的 ExpectedException 屬性(Attribute),可以方便我們達成目錄,以下為修改後的測試程式︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[TestClass]
public class CalculatorTest
{
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Add_第一個數值為負數_應拋出例外()
{
// Arrange
var target = new Calculator();

// Act
target.Add(-3, 4);
}
}

這樣的測試程式,看似完美的解決上述可讀性問題,但仍然有個缺點是,ExpectedException 屬性只能協助確認,Add() 是否有拋出 ArgumentException 例外,但谷無法讓我們驗証 Exception 的訊息是否為我們所預期的?

還有沒有更好的做法呢?

回到目錄

2.3 驗証是否拋出例外 - 使用 FluentAssertions

最後為您介紹一個好用工具︰FluentAssertions,可以完美解決上述問題。使用 FluentAssertions 後的測試程式碼如下︰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[TestClass]
public class CalculatorTest
{
[TestMethod]
public void Add_第一個數值為負數_應拋出例外()
{
// Arrange
var target = new Calculator();

// Act
Action comparison = () => { target.Add(-3, 4); };

// Assert
comparison.ShouldThrow<ArgumentException>().WithMessage("參數 number1 應為大於等於 0 的正整數");
}
}

可以看到,搭配了 FluentAssertions,我們既可以維持測試程式的可讀性,又可以方便達成我們的測試目標,方便使用。

回到目錄

3. 驗証測試物件狀態

上述的例子中,都只是驗証方法(Method)回傳的值是否正確,但別忘記了,C# 是 OOP 語言,很多時候我們呼叫物件的方法時,方法本身除了回傳結果值之外,可能也會連帶改變了物件本身的狀態(State)值,那我們該如何進行這方面的測試呢?

舉個例子,假設我們有一個銀行帳戶 BankAccount 類別,本類別中維護了帳戶中的餘額(Balance)狀態,而一旦存戶存錢(DepositMondey)後,帳戶中的餘額就會發生變動。我們的 production code 示意如下︰

Production Code (BankAccount)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class BankAccount
{
/// <summary>
/// 帳號餘額
/// </summary>
public int Balance { get; set; }

/// <summary>
/// Bank Account 建構子
/// </summary>
/// <param name="initialAmount">帳戶初始餘額</param>
public BankAccount(int initialAmount)
{
this.Balance = initialAmount;
}

/// <summary>
/// 存款
/// </summary>
/// <param name="amount">存入數量</param>
public void DepositMoney(int amount)
{
this.Balance += amount;
}
}

針對狀態的驗証,我們就不是單純驗証方法的回傳值,而是改為驗証方法執行後,變更後的物件狀態是否為我們預期的值。如下︰

Test Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[TestClass]
public class BankAccountTest
{
[TestMethod]
public void DepositMoney_存款100元進餘額為33元的帳戶中_帳戶餘額應為133元()
{
// Arrange
var target = new BankAccount(33);

// Act
target.DepositMoney(100);

// Assert
var expected = 133;
Assert.AreEqual(expected, target.Balance);
}
}

可以看到我們驗証的對象不再是 DepositMoney() 的回傳值(事實上 DepositMoney() 的回傳值型別為 void),而改為驗証物件的 Balance 狀態是否為我們預期的值。

回到目錄

小結

本文針對如何使用 MSTest 撰寫測試程式,做了簡單的說明。但目前我們看到的範例,都只有 BankAccount 一個類別單獨存在。實務上我們進行程式開發,當然不會那麼單純。在下一篇文章中,將針對相依性物件的測試方式進行說明,我們下次見。

回到目錄

References

Proudly powered by Hexo and Theme by Hacker
© 2018 LittleLin