Claude-skill-registry dotnet-testing-awesome-assertions-guide
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/dotnet-testing-awesome-assertions-guide" ~/.claude/skills/majiayu000-claude-skill-registry-dotnet-testing-awesome-assertions-guide && rm -rf "$T"
skills/data/dotnet-testing-awesome-assertions-guide/SKILL.mdAwesomeAssertions 流暢斷言指南
本技能提供使用 AwesomeAssertions 進行高品質測試斷言的完整指南,涵蓋基礎語法、進階技巧與最佳實踐。
關於 AwesomeAssertions
AwesomeAssertions 是 FluentAssertions 的社群分支版本,使用 Apache 2.0 授權,完全免費且無商業使用限制。
核心特色
- ✅ 完全免費:Apache 2.0 授權,適合商業專案使用
- 🔗 流暢語法:支援方法鏈結的自然語言風格
- 📦 豐富斷言:涵蓋物件、集合、字串、數值、例外等各種類型
- 💬 優秀錯誤訊息:提供詳細且易理解的失敗資訊
- ⚡ 高性能:優化的實作確保測試執行效率
- 🔧 可擴展:支援自訂 Assertions 方法
與 FluentAssertions 的關係
AwesomeAssertions 是 FluentAssertions 的社群 fork,主要差異:
| 項目 | FluentAssertions | AwesomeAssertions |
|---|---|---|
| 授權 | 商業專案需付費 | Apache 2.0(完全免費) |
| 命名空間 | | |
| API 相容性 | 原版 | 高度相容 |
| 社群支援 | 官方維護 | 社群維護 |
安裝與設定
NuGet 套件安裝
# .NET CLI dotnet add package AwesomeAssertions # Package Manager Console Install-Package AwesomeAssertions
csproj 設定(推薦)
<ItemGroup> <PackageReference Include="AwesomeAssertions" Version="9.1.0" PrivateAssets="all" /> </ItemGroup>
命名空間引用
using AwesomeAssertions; using Xunit;
核心 Assertions 語法
1. 物件斷言(Object Assertions)
基本檢查
[Fact] public void Object_基本斷言_應正常運作() { var user = new User { Id = 1, Name = "John", Email = "john@example.com" }; // 空值檢查 user.Should().NotBeNull(); // 類型檢查 user.Should().BeOfType<User>(); user.Should().BeAssignableTo<IUser>(); // 相等性檢查 var anotherUser = new User { Id = 1, Name = "John", Email = "john@example.com" }; user.Should().BeEquivalentTo(anotherUser); }
屬性驗證
[Fact] public void Object_屬性驗證_應正常運作() { var user = new User { Id = 1, Name = "John", Email = "john@example.com" }; // 單一屬性驗證 user.Id.Should().Be(1); user.Name.Should().Be("John"); user.Email.Should().Contain("@"); // 多屬性驗證 user.Should().BeEquivalentTo(new { Id = 1, Name = "John" }); }
2. 字串斷言(String Assertions)
內容驗證
[Fact] public void String_內容驗證_應正常運作() { var text = "Hello World"; // 基本檢查 text.Should().NotBeNullOrEmpty(); text.Should().NotBeNullOrWhiteSpace(); // 內容檢查 text.Should().Contain("Hello"); text.Should().StartWith("Hello"); text.Should().EndWith("World"); // 精確匹配 text.Should().Be("Hello World"); text.Should().BeEquivalentTo("hello world"); // 忽略大小寫 }
模式匹配
[Fact] public void String_模式匹配_應正常運作() { var email = "user@example.com"; // 正規表示式匹配 email.Should().MatchRegex(@"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"); // 長度驗證 email.Should().HaveLength(16); email.Should().HaveLengthGreaterThan(10); email.Should().HaveLengthLessThanOrEqualTo(50); }
3. 數值斷言(Numeric Assertions)
範圍與比較
[Fact] public void Numeric_範圍檢查_應正常運作() { var value = 10; // 比較運算 value.Should().BeGreaterThan(5); value.Should().BeLessThan(15); value.Should().BeGreaterThanOrEqualTo(10); value.Should().BeLessThanOrEqualTo(10); // 範圍檢查 value.Should().BeInRange(5, 15); value.Should().BeOneOf(8, 9, 10, 11); }
浮點數處理
[Fact] public void Numeric_浮點數精度_應正常運作() { var pi = 3.14159; // 精度比較 pi.Should().BeApproximately(3.14, 0.01); // 特殊值檢查 double.NaN.Should().Be(double.NaN); double.PositiveInfinity.Should().BePositiveInfinity(); // 符號檢查 pi.Should().BePositive(); (-5.5).Should().BeNegative(); }
4. 集合斷言(Collection Assertions)
基本檢查
[Fact] public void Collection_基本驗證_應正常運作() { var numbers = new[] { 1, 2, 3, 4, 5 }; // 數量檢查 numbers.Should().NotBeEmpty(); numbers.Should().HaveCount(5); numbers.Should().HaveCountGreaterThan(3); // 內容檢查 numbers.Should().Contain(3); numbers.Should().ContainSingle(x => x == 3); numbers.Should().NotContain(0); // 完整比對 numbers.Should().Equal(1, 2, 3, 4, 5); numbers.Should().BeEquivalentTo(new[] { 5, 4, 3, 2, 1 }); // 忽略順序 }
順序與唯一性
[Fact] public void Collection_順序驗證_應正常運作() { var numbers = new[] { 1, 2, 3, 4, 5 }; // 順序檢查 numbers.Should().BeInAscendingOrder(); numbers.Should().BeInDescendingOrder(); // 唯一性檢查 numbers.Should().OnlyHaveUniqueItems(); // 子集檢查 numbers.Should().BeSubsetOf(new[] { 1, 2, 3, 4, 5, 6, 7 }); numbers.Should().Contain(x => x > 3); }
複雜物件集合
[Fact] public void Collection_複雜物件_應正常運作() { var users = new[] { new User { Id = 1, Name = "John", Age = 30 }, new User { Id = 2, Name = "Jane", Age = 25 }, new User { Id = 3, Name = "Bob", Age = 35 } }; // 條件過濾 users.Should().Contain(u => u.Name == "John"); users.Should().OnlyContain(u => u.Age >= 18); // 全部滿足 users.Should().AllSatisfy(u => { u.Id.Should().BeGreaterThan(0); u.Name.Should().NotBeNullOrEmpty(); }); // LINQ 整合 users.Where(u => u.Age > 30).Should().HaveCount(1); }
5. 例外斷言(Exception Assertions)
基本例外處理
[Fact] public void Exception_基本驗證_應正常運作() { var service = new UserService(); // 預期拋出例外 Action act = () => service.GetUser(-1); act.Should().Throw<ArgumentException>() .WithMessage("*User ID*") .And.ParamName.Should().Be("userId"); }
不應拋出例外
[Fact] public void Exception_不應拋出_應正常運作() { var calculator = new Calculator(); // 不應拋出任何例外 Action act = () => calculator.Add(1, 2); act.Should().NotThrow(); // 不應拋出特定例外 act.Should().NotThrow<DivideByZeroException>(); }
巢狀例外
[Fact] public void Exception_巢狀例外_應正常運作() { var service = new DatabaseService(); Action act = () => service.Connect("invalid"); act.Should().Throw<DatabaseConnectionException>() .WithInnerException<ArgumentException>() .WithMessage("*connection string*"); }
6. 非同步斷言(Async Assertions)
Task 完成驗證
[Fact] public async Task Async_任務完成_應正常運作() { var service = new UserService(); // 等待任務完成 var task = service.GetUserAsync(1); await task.Should().CompleteWithinAsync(TimeSpan.FromSeconds(5)); // 驗證結果 task.Result.Should().NotBeNull(); task.Result.Id.Should().Be(1); }
非同步例外
[Fact] public async Task Async_例外處理_應正常運作() { var service = new ApiService(); Func<Task> act = async () => await service.CallInvalidEndpointAsync(); await act.Should().ThrowAsync<HttpRequestException>() .WithMessage("*404*"); }
進階技巧:複雜物件比對
深度物件比較
完整物件比對
[Fact] public void ComplexObject_深度比較_應正常運作() { var expected = new Order { Id = 1, CustomerName = "John Doe", Items = new[] { new OrderItem { ProductId = 1, Quantity = 2, Price = 10.5m }, new OrderItem { ProductId = 2, Quantity = 1, Price = 25.0m } }, TotalAmount = 46.0m, CreatedAt = DateTime.Now }; var actual = orderService.CreateOrder(orderRequest); // 深度物件比較 actual.Should().BeEquivalentTo(expected); }
排除特定屬性
[Fact] public void ComplexObject_排除屬性_應正常運作() { var user = userService.CreateUser("john@example.com"); user.Should().BeEquivalentTo(new { Email = "john@example.com", IsActive = true }, options => options .Excluding(u => u.Id) // 排除自動生成的 ID .Excluding(u => u.CreatedAt) // 排除時間戳記 .Excluding(u => u.UpdatedAt) ); }
動態欄位排除
[Fact] public void ComplexObject_動態排除_應正常運作() { var entity = entityService.CreateEntity(data); // 使用模式排除所有時間相關欄位 entity.Should().BeEquivalentTo(expectedEntity, options => options .Excluding(ctx => ctx.Path.EndsWith("At")) .Excluding(ctx => ctx.Path.EndsWith("Time")) .Excluding(ctx => ctx.Path.Contains("Timestamp")) ); }
循環參考處理
[Fact] public void ComplexObject_循環參考_應正常運作() { var parent = new TreeNode { Value = "Root" }; var child = new TreeNode { Value = "Child", Parent = parent }; parent.Children = new[] { child }; var actualTree = treeService.GetTree("Root"); // 處理循環參考 actualTree.Should().BeEquivalentTo(parent, options => options .IgnoringCyclicReferences() .WithMaxRecursionDepth(10) ); }
進階技巧:自訂 Assertions 擴展
領域特定 Assertions
建立專案特定的斷言方法,提升測試可讀性與可維護性。
範例:電商領域 Assertions
參考 templates/custom-assertions-template.cs 瞭解完整實作。
public static class ECommerceAssertions { public static AndConstraint<ObjectAssertions> BeValidProduct( this ObjectAssertions assertions) { var product = assertions.Subject as Product; product.Should().NotBeNull(); product!.Id.Should().BeGreaterThan(0); product.Name.Should().NotBeNullOrEmpty(); product.Price.Should().BeGreaterThan(0); return new AndConstraint<ObjectAssertions>(assertions); } public static AndConstraint<ObjectAssertions> BeValidOrder( this ObjectAssertions assertions) { var order = assertions.Subject as Order; order.Should().NotBeNull(); order!.Items.Should().NotBeNullOrEmpty(); order.TotalAmount.Should().BeGreaterThan(0); return new AndConstraint<ObjectAssertions>(assertions); } }
使用自訂 Assertions
[Fact] public void Product_建立產品_應為有效產品() { var product = productService.Create("Laptop", 999.99m); // 使用領域特定斷言 product.Should().BeValidProduct(); product.Name.Should().Be("Laptop"); }
可重用排除擴展
public static class SmartExclusionExtensions { public static EquivalencyOptions<T> ExcludingAutoGeneratedFields<T>( this EquivalencyOptions<T> options) { return options .Excluding(ctx => ctx.Path.EndsWith("Id") && ctx.SelectedMemberInfo.Name.StartsWith("Generated")) .Excluding(ctx => ctx.Path.EndsWith("At")) .Excluding(ctx => ctx.Path.Contains("Version")) .Excluding(ctx => ctx.Path.Contains("Timestamp")); } public static EquivalencyOptions<T> ExcludingAuditFields<T>( this EquivalencyOptions<T> options) { return options .Excluding(ctx => ctx.Path.Contains("CreatedBy")) .Excluding(ctx => ctx.Path.Contains("CreatedAt")) .Excluding(ctx => ctx.Path.Contains("ModifiedBy")) .Excluding(ctx => ctx.Path.Contains("ModifiedAt")); } }
使用範例:
[Fact] public void Entity_比對_應使用智慧排除() { var user = userService.CreateUser("test@example.com"); var retrieved = userService.GetUser(user.Id); retrieved.Should().BeEquivalentTo(user, options => options .ExcludingAutoGeneratedFields() .ExcludingAuditFields() ); }
效能最佳化策略
大量資料斷言
處理大量資料時的最佳實踐:
[Fact] public void LargeCollection_效能優化_應快速執行() { var largeDataset = Enumerable.Range(1, 100000) .Select(i => new DataRecord { Id = i, Value = $"Record_{i}" }) .ToList(); var processed = dataProcessor.ProcessLargeDataset(largeDataset); // 快速數量檢查 processed.Should().HaveCount(largeDataset.Count); // 抽樣驗證(避免全量比對) var sampleSize = Math.Min(1000, processed.Count / 10); var sampleIndices = Enumerable.Range(0, sampleSize) .Select(i => Random.Shared.Next(processed.Count)) .Distinct() .ToList(); foreach (var index in sampleIndices) { processed[index].Should().NotBeNull(); processed[index].Id.Should().BeGreaterThan(0); } }
選擇性屬性比對
[Fact] public void ComplexObject_選擇性比對_應提升效能() { var order = orderService.CreateOrder(request); // 只比對關鍵屬性,而非全物件掃描 order.Should().BeEquivalentTo(new { CustomerId = 123, TotalAmount = 999.99m, Status = "Pending" }, options => options .ExcludingMissingMembers() ); }
最佳實踐與團隊標準
測試命名規範
遵循
方法_情境_預期結果 模式:
public class UserServiceTests { [Fact] public void CreateUser_有效電子郵件_應回傳啟用的使用者() { // Arrange var email = "john@example.com"; // Act var user = userService.CreateUser(email); // Assert user.Should().NotBeNull(); user.Email.Should().Be(email); user.IsActive.Should().BeTrue(); } [Theory] [InlineData("", "Email cannot be empty")] [InlineData(null, "Email cannot be null")] public void CreateUser_無效電子郵件_應拋出參數例外( string invalidEmail, string expectedMessage) { Action act = () => userService.CreateUser(invalidEmail); act.Should().Throw<ArgumentException>() .WithMessage($"*{expectedMessage}*"); } }
錯誤訊息優化
提供清晰的失敗上下文:
[Fact] public void Payment_無效金額_應提供詳細錯誤() { var payment = new PaymentRequest { Amount = -100 }; var result = paymentService.ProcessPayment(payment); // 提供詳細的失敗原因 result.IsSuccess.Should().BeFalse( "because negative payment amounts are not allowed"); result.ErrorMessage.Should().Contain("amount", "because error message should specify the problematic field"); result.ErrorCode.Should().Be("INVALID_AMOUNT", "because specific error codes help with troubleshooting"); }
AssertionScope 使用
收集多個失敗訊息:
[Fact] public void User_完整驗證_應收集所有失敗() { var user = userService.CreateUser(testData); using (new AssertionScope()) { user.Should().NotBeNull("User creation should not fail"); user.Id.Should().BeGreaterThan(0, "User should have valid ID"); user.Email.Should().NotBeNullOrEmpty("Email is required"); user.IsActive.Should().BeTrue("New users should be active"); } // 所有失敗的斷言會一次顯示 }
常見情境與解決方案
情境 1:API 回應驗證
[Fact] public void API_使用者資料_應符合規格() { var response = apiClient.GetUserProfile(userId); response.StatusCode.Should().Be(200); response.Content.Should().NotBeNullOrEmpty(); var user = JsonSerializer.Deserialize<User>(response.Content); user.Should().BeEquivalentTo(new { Id = userId, Email = expectedEmail }, options => options .Including(u => u.Id) .Including(u => u.Email) ); }
情境 2:資料庫實體驗證
[Fact] public void Database_儲存實體_應正確持久化() { var user = new User { Name = "John", Email = "john@example.com" }; dbContext.Users.Add(user); dbContext.SaveChanges(); var saved = dbContext.Users.Find(user.Id); saved.Should().BeEquivalentTo(user, options => options .Excluding(u => u.CreatedAt) .Excluding(u => u.UpdatedAt) .Excluding(u => u.RowVersion) ); }
情境 3:事件驗證
[Fact] public void Event_發佈事件_應包含正確資料() { var eventRaised = false; OrderCreatedEvent? capturedEvent = null; eventBus.Subscribe<OrderCreatedEvent>(e => { eventRaised = true; capturedEvent = e; }); orderService.CreateOrder(orderRequest); eventRaised.Should().BeTrue("Order creation should raise event"); capturedEvent.Should().NotBeNull(); capturedEvent!.OrderId.Should().BeGreaterThan(0); capturedEvent.TotalAmount.Should().Be(expectedAmount); }
疑難排解
問題 1:BeEquivalentTo 失敗但物件看起來相同
原因:可能包含自動生成欄位或時間戳記
解決方案:
// 排除動態欄位 actual.Should().BeEquivalentTo(expected, options => options .Excluding(x => x.Id) .Excluding(x => x.CreatedAt) .Excluding(x => x.UpdatedAt) );
問題 2:集合順序不同導致失敗
原因:集合順序不同
解決方案:
// 使用 BeEquivalentTo 忽略順序 actual.Should().BeEquivalentTo(expected); // 不檢查順序 // 或明確指定需要檢查順序 actual.Should().Equal(expected); // 檢查順序
問題 3:浮點數比較失敗
原因:浮點數精度問題
解決方案:
// 使用精度容差 actualValue.Should().BeApproximately(expectedValue, 0.001);
何時使用此技能
適用情境
✅ 撰寫單元測試或整合測試時 ✅ 需要驗證複雜物件結構時 ✅ 比對 API 回應或資料庫實體時 ✅ 需要清晰的失敗訊息時 ✅ 建立領域特定測試標準時
不適用情境
❌ 效能測試(使用專用 benchmarking 工具) ❌ 負載測試(使用 K6、JMeter 等) ❌ UI 測試(使用 Playwright、Selenium)
與其他技能的配合
與 unit-test-fundamentals 搭配
先使用
unit-test-fundamentals 建立測試結構,再使用本技能撰寫斷言:
[Fact] public void Calculator_Add_兩個正數_應回傳總和() { // Arrange - 遵循 3A Pattern var calculator = new Calculator(); // Act var result = calculator.Add(2, 3); // Assert - 使用 AwesomeAssertions result.Should().Be(5); }
與 test-naming-conventions 搭配
使用
test-naming-conventions 的命名規範,搭配本技能的斷言:
[Fact] public void CreateUser_有效資料_應回傳啟用使用者() { var user = userService.CreateUser("test@example.com"); user.Should().NotBeNull() .And.BeOfType<User>(); user.IsActive.Should().BeTrue(); }
與 xunit-project-setup 搭配
在
xunit-project-setup 建立的專案中安裝並使用 AwesomeAssertions。
參考資源
原始文章
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
-
Day 04 - AwesomeAssertions 基礎應用與實戰技巧
-
Day 05 - AwesomeAssertions 進階技巧與複雜情境應用
官方資源
- AwesomeAssertions GitHub:https://github.com/AwesomeAssertions/AwesomeAssertions
- AwesomeAssertions 官方文件:https://awesomeassertions.org/
相關文章
- Fluent Assertions 授權變化討論:https://www.dotblogs.com.tw/mrkt/2025/04/19/152408
總結
AwesomeAssertions 提供了強大且可讀的斷言語法,是撰寫高品質測試的重要工具。透過:
- 流暢語法:讓測試程式碼更易讀
- 豐富斷言:涵蓋各種資料類型
- 自訂擴展:建立領域特定斷言
- 效能優化:處理大量資料情境
- 完全免費:Apache 2.0 授權無商業限制
記住:好的斷言不僅能驗證結果,更能清楚表達預期行為,並在失敗時提供有用的診斷資訊。
參考 templates/assertion-examples.cs 查看更多實用範例。