Unit Test

單元測試的基本結構與三種 .NET 測試框架的屬性標籤對照,包含各自的生命週期事件與參數化測試寫法。

3A 原則

Arrange:安排/準備

在這一步你準備所有測試要用到的東西。包括被測試物件 (SUT, System Under Test)、依賴(例如模擬物件 mock、stub)、輸入參數、預期結果,設定測試環境或上下文等等。

Act:執行/行動

在這一步調用你的程式碼中要測試的方法或函式,做實際的操作。就是把輸入傳進去,讓它執行。

Assert:斷言/驗證

最後檢查操作結果是否如你預期的那樣。把實際結果和預期結果比較,有沒有錯誤。這部份可以用測試框架提供的 assert 函數或方法。

Unit Test Framework

社群用量比較 xUnit > NUnit > MSTest,其中 xUnit 具備高靈活性與現代特性,更易與持續整合(CI/CD)工具整合。

(1) 字串比較

比較是否(不)相符:

框架 方法
nUnit Assert.AreEqual(Assert.AreNotEqual)
xUnit Assert.Equal(Assert.NotEqual)

比較字串 1 是否(不)存在在字串 2:

框架 方法
nUnit  
xUnit Assert.Contains(Assert.NotContains)

(2) 布林比較

  • Assert.True:比較結果是否為 true
  • Assert.False:比較結果是否為 false

(3) Null Reference 比較

  • Assert.Null(Assert.NotNull):比較物件是否(不)為 null

(4) 物件比較

  • Assert.Same(Assert.NotSame):比較兩物件是否(不)相同

(5) 數值比較

  • Assert.InRange(Assert.NotInRange):比較數值是否(不)在範圍中

Event Hooks(測試生命週期事件)

是指測試框架提供的一組方法,這些方法可以在測試生命週期中的特定時間點執行某些操作。

一、MSTest

  • TestClass:標示測試類別屬性。
  • TestMethod:標示這是測試方法。
  • AssemblyInitialize:所有測試執行前只會跑一次。方法需是靜態。
  • ClassInitialize:在測試類別中的所有測試方法執行之前執行一次。方法需是靜態。
  • ClassCleanup:在測試類別中的所有測試方法執行後執行一次。方法需是靜態。
  • TestInitialize:在每個測試方法執行前執行一次。
  • TestCleanup:在每個測試方法執行後執行一次。
  • AssemblyCleanup:所有測試結束後跑一次。
  • TestCategory:設定測試分類。範例:TestCategory("Math")。
  • DataRow:執行多筆資料的測試。
  • DataTestMethod:執行多筆資料的測試。

範例:

[TestClass]
public class CalculatorTests
{
    [DataTestMethod]
    [DataRow(1, 2, 3)]
    [DataRow(5, 5, 10)]
    [DataRow(-1, 1, 0)]
    public void Add_TestMultipleInputs(int a, int b, int expected)
    {
        var calc = new Calculator();
        var result = calc.Add(a, b);
        Assert.AreEqual(expected, result);
    }
}
  • Ignore:忽略此測試,不執行。
  • ExpectedException:驗證某個測試會丟出指定的例外。

範例:

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Test_ThrowsException()
{
    throw new ArgumentNullException();
}

二、NUnit

  • Test:標示這是測試方法。
  • Ignore:跳過此測試。
  • SetUp:在每個測試方法之前執行。
  • TearDown:在每個測試方法之後執行。
  • OneTimeSetUp:在所有測試方法之前執行一次。
  • OneTimeTearDown:在所有測試方法之後執行一次。
  • TestCase:傳遞不同參數組合。

範例:

[TestCase(1, 2, 3)]
[TestCase(-1, -2, -3)]
public void Add_WithVariousInputs_ReturnsExpected(int a, int b, int expected)
{
    var calc = new Calculator();
    var result = calc.Add(a, b);
    Assert.AreEqual(expected, result);
}
  • Category:可將測試方法分類,方便篩選執行。範例:[Category("Math")]。
  • TestFixture:標記的類別可以與多個測試共享同一個上下文。

範例:

[TestFixture(1, 2, 3)]
[TestFixture(2, 3, 5)]
public class AddTests
{
    private readonly int _a, _b, _expected;

    public AddTests(int a, int b, int expected)
    {
        _a = a;
        _b = b;
        _expected = expected;
    }

    [Test]
    public void TestAddition()
    {
        var calc = new Calculator();
        Assert.AreEqual(_expected, calc.Add(_a, _b));
    }
}

三、xUnit

  • Fact:標記一個簡單的測試方法。
  • Theory:傳入不同的資料進行多次測試。

範例:

[Theory]
[InlineData(1, 2, 3)]
[InlineData(3, 4, 7)]
public void Add_ValidNumbers_ReturnsCorrectSum(int a, int b, int expected)
{
    var calc = new Calculator();
    var result = calc.Add(a, b);
    Assert.Equal(expected, result);
}
  • Skip:跳過測試。
  • Trait:為測試分類。