在 C# 單元測試中使用 Fluent Assertions ,用更容易閱讀的方式寫測試

Fluent Assertions 簡介

Fluent Assertions 是一個第三方套件,是一個斷言框架 (assertion framework),指在讓測試程式碼更容易閱讀、提供更容易除錯的測試失敗訊息。
支援多種測試框架,包含 MSTest2, xUnit2, NUnit3, MSpec 和 NSpec3, 並且在 Json.NET, Ninject, Autofac, ASP.NET MVC, ASP.NET Core MVC 上也有特別支援的套件可以使用。

原本的測試程式碼

筆者平時在寫 C# 的單元測試時,都是使用 NUnit 測試框架,先來看一段很簡單的測試:

    [Test]
    public void Test1()
    {
        const int input1 = 1;
        const int input2 = 2;
        const int expected = 3;

        Assert.AreEqual(expected, input1 + input2);
    }
    

上面這段程式碼在 Rider 開發工具中會有建議提示:
    
Consider using the constraint model, Assert.That(actual, Is.EqualTo(expected)), instead of the classic model, Assert.AreEqual(expected, actual)
    

意思是指 Assert.AreEqual 是經典模型(classic model),叫我們可以替換為 Assert.That 的約束模型(constraint model)

兩者的比較如下:
    
// 經典模型
Assert.AreEqual(expected, input1 + input2);

// 約束模型
Assert.That(input1 + input2, Is.EqualTo(expected));
    

StackOverflow 上也有人在討論這兩種的方式,筆者比較喜歡經典模型,不過不管哪種方式似乎都不太容易閱讀

安裝 Fluent Assertions

使用 NuGet 安裝 FluentAssertions 套件,或是使用 .NET CLI 指令安裝:
	
dotnet add package FluentAssertions
    

使用 Fluent Assertions 的測試程式碼

回到上面的範例,看一下使用 Fluent Assertions 後的測試程式碼:
    
    [Test]
    public void Test1()
    {
        const int input1 = 1;
        const int input2 = 2;
        const int expected = 3;

        // 經典模型
        Assert.AreEqual(expected, input1 + input2);

        // 約束模型
        Assert.That(input1 + input2, Is.EqualTo(expected));

        // 使用 Fluent Assertions
        (input1 + input2).Should().Be(expected);
    }
    

Fluent Assertions 示範

    
    [Test]
    public void Test2()
    {
        // 比對文字
        var userNames = "testUser01";
        // 錯誤示範,錯誤訊息: Expected userNames to be "Ruyut" with a length of 5, but "testUser01" has a length of 10, differs near "tes" (index 0).
        // userNames.Should().Be("Ruyut");
        // 正確:
        userNames.Should().Be("testUser01");

        // 例外捕捉
        Action action = () => { throw new Exception("this is a exception"); };
        // 錯誤示範,錯誤訊息: Expected exception message to match the equivalent of "this is a exception!!!", but "this is a exception" does not.
        // action.Should().Throw<Exception>().WithMessage("this is a exception!!!"); 
        // 正確:
        action.Should().Throw<Exception>().WithMessage("this is a exception");

        // 比對集合
        IEnumerable<int> numbers = new[] { 1, 2, 3 };
        // 錯誤示範,錯誤訊息: Expected numbers to contain only items matching (n == 0), but {1, 2, 3} do(es) not match.
        // numbers.Should().OnlyContain(n => n == 0); // 正確: numbers.Should().OnlyContain(n => n > 0);
        // 正確:
        numbers.Should().OnlyContain(n => n > 0);

        // 比對日期時間
        var dateTime = new DateTime(2022, 1, 1, 20, 30, 0);
        // 錯誤示範,錯誤訊息: Expected the seconds part of dateTime to be 30, but found 0.
        // dateTime.Should().HaveYear(2022).And.HaveMonth(1).And.HaveDay(1).And.HaveHour(20).And.HaveMinute(30).And.HaveSecond(30);
        // 正確寫法 1:
        dateTime.Should().Be(new DateTime(2022, 1, 1, 20, 30, 0));
        // 正確寫法 2:
        dateTime.Should().HaveYear(2022).And.HaveMonth(1).And.HaveDay(1).And.HaveHour(20).And.HaveMinute(30).And.HaveSecond(0);

        // 比對列舉
        TestEnum testEnum = TestEnum.Test1;
        // 錯誤示範,錯誤訊息: Expected testEnum to be TestEnum.Test2 {value: 1}, but found TestEnum.Test1 {value: 0}.
        // testEnum.Should().Be(TestEnum.Test2);
        // 正確:
        testEnum.Should().Be(TestEnum.Test1);

        // 其他示範
        testEnum.Should().HaveValue(0);
        ((TestEnum)2).Should().BeDefined();
        ((TestEnum)3).Should().NotBeDefined(); // 因為只有到 2
    }

    public enum TestEnum
    {
        Test1 = 0,
        Test2,
        Test3,
    }
    

在開始使用 Fluent Assertions 寫測試之後,筆者覺得程式碼的可讀性有變高,程式碼也有變簡潔,同個變數要加上多個測試內容也可以很方便的使用 And 連接即可,非常推薦大家使用 Fluent Assertions

參考資料:
Fluent Assertions
FluentAssertions - GitHub

留言