dotnet-testing-awesome-assertions-guide
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-awesome-assertions-guide
Agent 安装分布
Skill 文档
AwesomeAssertions æµæ¢æ·è¨æå
æ¬æè½æä¾ä½¿ç¨ AwesomeAssertions é²è¡é«å質測試æ·è¨ç宿´æåï¼æ¶µèåºç¤èªæ³ãé²éæå·§èæä½³å¯¦è¸ã
éæ¼ AwesomeAssertions
AwesomeAssertions æ¯ FluentAssertions çç¤¾ç¾¤åæ¯çæ¬ï¼ä½¿ç¨ Apache 2.0 ææ¬ï¼å®å ¨å è²»ä¸ç¡åæ¥ä½¿ç¨éå¶ã
æ ¸å¿ç¹è²
- â å®å ¨å è²»ï¼Apache 2.0 ææ¬ï¼é©å忥尿¡ä½¿ç¨
- ð æµæ¢èªæ³ï¼æ¯æ´æ¹æ³éçµçèªç¶èªè¨é¢¨æ ¼
- ð¦ è±å¯æ·è¨ï¼æ¶µèç©ä»¶ãéåãåä¸²ãæ¸å¼ãä¾å¤çå種é¡å
- ð¬ åªç§é¯èª¤è¨æ¯ï¼æä¾è©³ç´°ä¸æçè§£ç失æè³è¨
- â¡ é«æ§è½ï¼åªåç實ä½ç¢ºä¿æ¸¬è©¦å·è¡æç
- ð§ 坿´å±ï¼æ¯æ´èªè¨ Assertions æ¹æ³
è FluentAssertions çéä¿
AwesomeAssertions æ¯ FluentAssertions ç社群 forkï¼ä¸»è¦å·®ç°ï¼
| é ç® | FluentAssertions | AwesomeAssertions |
|---|---|---|
| ææ¬ | 忥尿¡éä»è²» | Apache 2.0ï¼å®å ¨å è²»ï¼ |
| å½å空é | FluentAssertions |
AwesomeAssertions |
| 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 åºç¤æç¨èå¯¦æ°æå·§
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10374188
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day04
-
Day 05 – AwesomeAssertions é²éæå·§èè¤éæ 墿ç¨
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10374425
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day05
宿¹è³æº
- 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 æ¥çæ´å¤å¯¦ç¨ç¯ä¾ã