C#單元測試中的流暢斷言
[[academy-video-youtube({"vid": "TytferBCLOo", "start_time": "0", "title": "Fluent Assertions in Unit Testing in C#", "creator": "Tim Corey", "length": "10m 6s"})]]
單元測試編寫一次但閱讀多次,因此斷言行是測試套件中最重要的細節之一。Assert.Equal(expected, actual) 可以運作,但是僅僅比較兩個字串的失敗訊息並不總是能解釋測試在檢查什麼。 流暢斷言重塑了斷言語法,使該行顯示為其強制執行的規則,並且故障訊息成為一個句子,而不是差異。
在他的影片"Fluent Assertions in Unit Testing in C#"中,Tim Corey 採用了已經練習 SampleClass 快樂路徑的 xUnit 專案,然後引入了一個邊界情況(Eddie Van Halen),這破壞了簡單的空格分割邏輯。 他安裝了流暢斷言包,使用 .Should().Be() 鏈重寫了斷言,展示了如何在單個值上連結多個條件,並以 AssertionScope 結束,這樣單個測試便同時報告每個失敗條件而不是在第一個失敗時放棄。 測試套件已經超越裸等函式清晰的團隊將在這裡找到升級路徑。
xUnit 的起始點
[0:35 - 2:46] 螢幕上的專案很小:一個類程式庫,帶有一個 SampleClass 它在構造函式中接受全名並將其按空格分割為 FirstName 和 LastName,再加上一個練習快樂路徑的 xUnit 測試專案。 測試使用具有 InlineData 屬性的理論來對兩個輸入(一個是"Tim Corey",另一個是"Sue Storm")進行相同邏輯測試,每個斷言都是標準形態 Assert.Equal(expected, actual)。
[Theory]
[InlineData("Tim Corey", "Tim")]
[InlineData("Sue Storm", "Sue")]
public void TestFirstNameProperty(string fullName, string expected)
{
var sample = new SampleClass(fullName);
Assert.Equal(expected, sample.FirstName);
}[Theory]
[InlineData("Tim Corey", "Tim")]
[InlineData("Sue Storm", "Sue")]
public void TestFirstNameProperty(string fullName, string expected)
{
var sample = new SampleClass(fullName);
Assert.Equal(expected, sample.FirstName);
}運行該套件給出了六個通過的測試。 此事物沒有錯,而且對於符合"first space last"的情況,其斷言讀起來相當清楚。 有趣的領域從不符合該格式的名字開始。
快樂路徑停止工作的地方
[2:46 - 4:32] Tim 選擇的邊界案例是"Eddie Van Halen",這是一個三個單詞的名字,其中姓氏是"Van Halen",而不是僅僅第一個空格之後的字元。 當前實作抓取分割索引1並稱之為姓氏,因此類返回"Van"作為姓氏,完全放棄了"Halen"。 該錯誤屬實,編寫一個失敗的測試是修正它的第一步。
using Assert.Equal("Van Halen", sample.LastName) 編寫該測試效果不錯,但故障訊息以不正明的字串比較呈現。 切換到流暢斷言進一步用語言表達規則,並生成一個失敗訊息,指出測試中的屬性。對於小型套件來說,差異看似外觀性的; 對於由幾百個斷言組成的套件而言,易讀性的差異是複合的。
安裝流暢斷言
[4:32 - 5:10] 該包位於 FluentAssertions 下的 NuGet 上。 Tim 將其標記為註冊表上最多下載的包之一,這是重要的背景資訊:任何新參與者都很有可能之前已經見過。 測試專案在頂部獲取了兩個 using 指令:
using FluentAssertions;
using FluentAssertions.Execution;using FluentAssertions;
using FluentAssertions.Execution;第一個引入了斷言擴展方法,這是大多數測試需要的。 第二個是為影片後面涉及的 AssertionScope 型別存在的; 不分組斷言的測試可以將其省略。
Should.Be 語法
[5:10 - 6:14] 最低限度的流暢斷言重寫 Van Halen 測試:
[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
sample.LastName.Should().Be("Van Halen");
}[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
sample.LastName.Should().Be("Van Halen");
}該鏈條讀起來接近口語英語。 .Should() 擴展返回一個斷言物件,其方法描述比較; .Be(...) 進行相等性檢查。 大小寫敏感性、前導和尾隨空格都是檢查的一部分,這符合大多數生產程式碼中字串比較的行為。
以此測試運行在損壞的實施上產生了一個失敗訊息,該訊息指出了待測屬性並解釋了差異:"預期 sample.LastName 為有長度 9 的 'Van Halen',但 'Van' 只有長度 3。" 該訊息由生成它的表達標識待測值,這是裸 Assert.Equal 形式沒有提供的那種背景。
連結多個條件
[6:14 - 8:10] 單個屬性通常需要滿足多個規則。 流暢斷言合成條件 .And 以保持鏈條停留在一個聲明上:
sample.LastName.Should()
.StartWith("Van")
.And.EndWith("len")
.And.Contain(" ");sample.LastName.Should()
.StartWith("Van")
.And.EndWith("len")
.And.Contain(" ");鏈條中的每個連結都是一個單獨的條件。 預設下,鏈條在第一次失敗時停止報告:如果 StartWith("Van") 通過但 EndWith("len") 失敗,失敗訊息將指出 EndWith 並且斷言停在那裡。 對於每個條件都獨立,並希望暴露每一次失敗的測試,斷言範圍(下一節)改變了該行為。
連結語法偏向於表達意圖而非枚舉相等。 說"姓氏應以 Van 開始,以 len 結束,並包含一個空格"的測試讀起來像一份規範。 用三個單獨的 Assert.True 調用編寫的邏輯同樣會讀作三個布林檢查,且沒有敘述將它們連接起來。
報告每一個失敗的斷言範圍
[8:10 - 9:34] 在第一次失敗時停止的預設行為對於依賴於較早條件的鏈條是可以的。 對於每一個條件獨立的鏈條,將斷言包裹於 AssertionScope 中使得測試一次報告每一個失敗:
[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
using var _ = new AssertionScope();
sample.LastName.Should().StartWith("Van");
sample.LastName.Should().EndWith("len");
sample.LastName.Should().Contain(" ");
}[Fact]
public void TestEdgeCaseNames()
{
var sample = new SampleClass("Eddie Van Halen");
using var _ = new AssertionScope();
sample.LastName.Should().StartWith("Van");
sample.LastName.Should().EndWith("len");
sample.LastName.Should().Contain(" ");
}使用-變數-丟棄行是標準模式:範圍持續到方法尾部變數超出範圍,丟棄下劃線表示變數本身從未讀取。 當測試失敗時,訊息包含每一個失敗條件作為單獨行,意味著一次運行給出了完整故事,而不是強迫三次運行揭露三個問題。 對於驗證跨多個屬性返回物件形狀的測試,這是每次測試迴圈修正一個錯誤與一次性修正全部的差異。
總結:像規範一樣讀的測試
[9:34 - 10:06] 流暢斷言不會改變測試驗證什麼; 它改變了驗證的讀取方式。 .Should() 連鎖、.And 合成和 AssertionScope 分組將測試程式碼推向行為待測的書面規範。與由 FluentValidation 用於輸入驗證的相同行風格結合,該模式產生使新手能夠無需介紹即閱讀的測試套件和驗證規則。
結論
[9:34 - 10:06] 將流暢斷言新增到測試專案僅需一個 NuGet 安裝、兩個 using 指示和從 Assert.Equal(expected, actual) 到 actual.Should().Be(expected) 的斷言行重寫。 連結表達多條件規則於單個語句中,而 AssertionScope 則使單個測試報告每一個失敗,而非在第一次失敗時停止。其收益是說明破壞規則的失敗資訊,這是行跡和句子之間的差異。
範例提示:斷言集合時,偏向 result.Should().BeEquivalentTo(expected) 而非逐屬性比較。 其為您遍曉物件圖,並按路徑報告第一個差異屬性(例如 users[2].Address.City),在 50 元素列表因一個巢狀字段不同意時,這比"集合不等"訊息要更有用。

