Claude-skill-registry dotnet-testing-autodata-xunit-integration
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
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-autodata-xunit-integration" ~/.claude/skills/majiayu000-claude-skill-registry-dotnet-testing-autodata-xunit-integration && rm -rf "$T"
manifest:
skills/data/dotnet-testing-autodata-xunit-integration/SKILL.mdsource content
AutoData 屬性家族:xUnit 與 AutoFixture 的整合應用
觸發關鍵字
- AutoData
- InlineAutoData
- MemberAutoData
- CompositeAutoData
- xUnit AutoFixture 整合
- 參數化測試資料
- 測試參數注入
- CollectionSizeAttribute
- 外部測試資料
- CSV 測試資料
- JSON 測試資料
概述
AutoData 屬性家族是
AutoFixture.Xunit2 套件提供的功能,將 AutoFixture 的資料產生能力與 xUnit 的參數化測試整合,讓測試參數自動注入,大幅減少測試準備程式碼。
核心特色
- AutoData:自動產生所有測試參數
- InlineAutoData:混合固定值與自動產生
- MemberAutoData:結合外部資料來源
- CompositeAutoData:多重資料來源整合
- CollectionSizeAttribute:控制集合產生數量
安裝套件
<PackageReference Include="AutoFixture" Version="4.18.1" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
dotnet add package AutoFixture.Xunit2
AutoData:完全自動產生參數
AutoData 是最基礎的屬性,自動為測試方法的所有參數產生測試資料。
基本使用
using AutoFixture.Xunit2; public class Person { public Guid Id { get; set; } [StringLength(10)] public string Name { get; set; } = string.Empty; [Range(18, 80)] public int Age { get; set; } public string Email { get; set; } = string.Empty; public DateTime CreateTime { get; set; } } [Theory] [AutoData] public void AutoData_應能自動產生所有參數(Person person, string message, int count) { // Arrange & Act - 參數已由 AutoData 自動產生 // Assert person.Should().NotBeNull(); person.Id.Should().NotBe(Guid.Empty); person.Name.Should().HaveLength(10); // 遵循 StringLength 限制 person.Age.Should().BeInRange(18, 80); // 遵循 Range 限制 message.Should().NotBeNullOrEmpty(); count.Should().NotBe(0); }
透過 DataAnnotation 約束參數
[Theory] [AutoData] public void AutoData_透過DataAnnotation約束參數( [StringLength(5, MinimumLength = 3)] string shortName, [Range(1, 100)] int percentage, Person person) { // Assert shortName.Length.Should().BeInRange(3, 5); percentage.Should().BeInRange(1, 100); person.Should().NotBeNull(); }
InlineAutoData:混合固定值與自動產生
InlineAutoData 結合了 InlineData 的固定值特性與 AutoData 的自動產生功能。
基本語法
[Theory] [InlineAutoData("VIP客戶", 1000)] [InlineAutoData("一般客戶", 500)] [InlineAutoData("新客戶", 100)] public void InlineAutoData_混合固定值與自動產生( string customerType, // 對應第 1 個固定值 decimal creditLimit, // 對應第 2 個固定值 Person person) // 由 AutoFixture 產生 { // Arrange var customer = new Customer { Person = person, Type = customerType, CreditLimit = creditLimit }; // Assert customer.Type.Should().Be(customerType); customer.CreditLimit.Should().BeOneOf(1000, 500, 100); customer.Person.Should().NotBeNull(); }
參數順序一致性
固定值的順序必須與方法參數順序一致:
[Theory] [InlineAutoData("產品A", 100)] // 依序對應 name, price [InlineAutoData("產品B", 200)] public void InlineAutoData_參數順序一致性( string name, // 對應第 1 個固定值 decimal price, // 對應第 2 個固定值 string description, // 由 AutoFixture 產生 Product product) // 由 AutoFixture 產生 { // Assert name.Should().BeOneOf("產品A", "產品B"); price.Should().BeOneOf(100, 200); description.Should().NotBeNullOrEmpty(); product.Should().NotBeNull(); }
⚠️ 重要限制:只能使用編譯時常數
// ✅ 正確:使用常數 [InlineAutoData("VIP", 100000)] [InlineAutoData("Premium", 50000)] // ❌ 錯誤:不能使用變數 private const decimal VipCreditLimit = 100000m; [InlineAutoData("VIP", VipCreditLimit)] // 編譯錯誤 // ❌ 錯誤:不能使用運算式 [InlineAutoData("VIP", 100 * 1000)] // 編譯錯誤
需要使用複雜資料時,應使用
MemberAutoData。
MemberAutoData:結合外部資料來源
MemberAutoData 允許從類別的方法、屬性或欄位中獲取測試資料。
使用靜態方法
public class MemberAutoDataTests { public static IEnumerable<object[]> GetProductCategories() { yield return new object[] { "3C產品", "TECH" }; yield return new object[] { "服飾配件", "FASHION" }; yield return new object[] { "居家生活", "HOME" }; yield return new object[] { "運動健身", "SPORTS" }; } [Theory] [MemberAutoData(nameof(GetProductCategories))] public void MemberAutoData_使用靜態方法資料( string categoryName, // 來自 GetProductCategories string categoryCode, // 來自 GetProductCategories Product product) // 由 AutoFixture 產生 { // Arrange var categorizedProduct = new CategorizedProduct { Product = product, CategoryName = categoryName, CategoryCode = categoryCode }; // Assert categorizedProduct.CategoryName.Should().Be(categoryName); categorizedProduct.CategoryCode.Should().Be(categoryCode); categorizedProduct.Product.Should().NotBeNull(); } }
使用靜態屬性
public static IEnumerable<object[]> StatusTransitions => new[] { new object[] { OrderStatus.Created, OrderStatus.Confirmed }, new object[] { OrderStatus.Confirmed, OrderStatus.Shipped }, new object[] { OrderStatus.Shipped, OrderStatus.Delivered }, new object[] { OrderStatus.Delivered, OrderStatus.Completed } }; [Theory] [MemberAutoData(nameof(StatusTransitions))] public void MemberAutoData_使用靜態屬性_訂單狀態轉換( OrderStatus fromStatus, OrderStatus toStatus, Order order) { // Arrange order.Status = fromStatus; // Act order.TransitionTo(toStatus); // Assert order.Status.Should().Be(toStatus); }
自訂 AutoData 屬性
建立專屬的 AutoData 配置:
DomainAutoDataAttribute
public class DomainAutoDataAttribute : AutoDataAttribute { public DomainAutoDataAttribute() : base(() => CreateFixture()) { } private static IFixture CreateFixture() { var fixture = new Fixture(); // 設定 Person 的自訂規則 fixture.Customize<Person>(composer => composer .With(p => p.Age, () => Random.Shared.Next(18, 65)) .With(p => p.Email, () => $"user{Random.Shared.Next(1000)}@example.com") .With(p => p.Name, () => $"測試用戶{Random.Shared.Next(100)}")); // 設定 Product 的自訂規則 fixture.Customize<Product>(composer => composer .With(p => p.Price, () => Random.Shared.Next(100, 10000)) .With(p => p.IsAvailable, true) .With(p => p.Name, () => $"產品{Random.Shared.Next(1000)}")); return fixture; } }
BusinessAutoDataAttribute
public class BusinessAutoDataAttribute : AutoDataAttribute { public BusinessAutoDataAttribute() : base(() => CreateFixture()) { } private static IFixture CreateFixture() { var fixture = new Fixture(); // 設定 Order 的業務規則 fixture.Customize<Order>(composer => composer .With(o => o.Status, OrderStatus.Created) .With(o => o.Amount, () => Random.Shared.Next(1000, 50000)) .With(o => o.OrderNumber, () => $"ORD{DateTime.Now:yyyyMMdd}{Random.Shared.Next(1000):D4}")); return fixture; } }
使用自訂 AutoData
[Theory] [DomainAutoData] public void 使用DomainAutoData(Person person, Product product) { person.Age.Should().BeInRange(18, 64); person.Email.Should().EndWith("@example.com"); product.IsAvailable.Should().BeTrue(); } [Theory] [BusinessAutoData] public void 使用BusinessAutoData(Order order) { order.Status.Should().Be(OrderStatus.Created); order.Amount.Should().BeInRange(1000, 49999); order.OrderNumber.Should().StartWith("ORD"); }
CompositeAutoData:多重資料來源整合
組合多個自訂 AutoData 配置:
public class CompositeAutoDataAttribute : AutoDataAttribute { public CompositeAutoDataAttribute(params Type[] autoDataAttributeTypes) : base(() => CreateFixture(autoDataAttributeTypes)) { } private static IFixture CreateFixture(Type[] autoDataAttributeTypes) { var fixture = new Fixture(); foreach (var attributeType in autoDataAttributeTypes) { var createFixtureMethod = attributeType.GetMethod( "CreateFixture", BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.FlattenHierarchy); if (createFixtureMethod != null) { var sourceFixture = (IFixture)createFixtureMethod.Invoke(null, null)!; foreach (var customization in sourceFixture.Customizations) { fixture.Customizations.Add(customization); } } } return fixture; } } // 使用方式 [Theory] [CompositeAutoData(typeof(DomainAutoDataAttribute), typeof(BusinessAutoDataAttribute))] public void CompositeAutoData_整合多重資料來源( Person person, Product product, Order order) { // DomainAutoData 的設定 person.Age.Should().BeInRange(18, 64); product.IsAvailable.Should().BeTrue(); // BusinessAutoData 的設定 order.Status.Should().Be(OrderStatus.Created); }
CollectionSizeAttribute:控制集合大小
AutoData 預設的集合大小是 3,可透過自訂屬性控制:
CollectionSizeAttribute 實作
using AutoFixture; using AutoFixture.Kernel; using AutoFixture.Xunit2; using System.Reflection; public class CollectionSizeAttribute : CustomizeAttribute { private readonly int _size; public CollectionSizeAttribute(int size) { _size = size; } public override ICustomization GetCustomization(ParameterInfo parameter) { ArgumentNullException.ThrowIfNull(parameter); var objectType = parameter.ParameterType.GetGenericArguments()[0]; var isTypeCompatible = parameter.ParameterType.IsGenericType && parameter.ParameterType.GetGenericTypeDefinition() .MakeGenericType(objectType) .IsAssignableFrom(typeof(List<>).MakeGenericType(objectType)); if (!isTypeCompatible) { throw new InvalidOperationException( $"{nameof(CollectionSizeAttribute)} 指定的型別與 List 不相容: " + $"{parameter.ParameterType} {parameter.Name}"); } var customizationType = typeof(CollectionSizeCustomization<>).MakeGenericType(objectType); return (ICustomization)Activator.CreateInstance(customizationType, parameter, _size)!; } private class CollectionSizeCustomization<T> : ICustomization { private readonly ParameterInfo _parameter; private readonly int _repeatCount; public CollectionSizeCustomization(ParameterInfo parameter, int repeatCount) { _parameter = parameter; _repeatCount = repeatCount; } public void Customize(IFixture fixture) { fixture.Customizations.Add( new FilteringSpecimenBuilder( new FixedBuilder(fixture.CreateMany<T>(_repeatCount).ToList()), new EqualRequestSpecification(_parameter))); } } }
使用 CollectionSizeAttribute
[Theory] [AutoData] public void CollectionSize_控制自動產生集合大小( [CollectionSize(5)] List<Product> products, [CollectionSize(3)] List<Order> orders, Customer customer) { // Assert products.Should().HaveCount(5); orders.Should().HaveCount(3); customer.Should().NotBeNull(); products.Should().AllSatisfy(product => { product.Name.Should().NotBeNullOrEmpty(); product.Price.Should().BeGreaterOrEqualTo(0); }); }
外部測試資料整合
測試專案檔案設定
在
.csproj 中設定外部資料檔案:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> </PropertyGroup> <ItemGroup> <!-- CSV 檔案 --> <Content Include="TestData\*.csv"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> <!-- JSON 檔案 --> <Content Include="TestData\*.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> </ItemGroup> <ItemGroup> <PackageReference Include="CsvHelper" Version="33.0.1" /> </ItemGroup> </Project>
CSV 檔案整合
TestData/products.csv
ProductId,Name,Category,Price,IsAvailable 1,"iPhone 15","3C產品",35900,true 2,"MacBook Pro","3C產品",89900,true 3,"AirPods Pro","3C產品",7490,false 4,"Nike Air Max","運動用品",4200,true
CSV 讀取與整合
using CsvHelper; using CsvHelper.Configuration; using System.Globalization; public class ExternalDataIntegrationTests { public static IEnumerable<object[]> GetProductsFromCsv() { var csvPath = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "products.csv"); using var reader = new StreamReader(csvPath); var config = new CsvConfiguration(CultureInfo.InvariantCulture) { HeaderValidated = null, MissingFieldFound = null }; using var csv = new CsvReader(reader, config); var records = csv.GetRecords<ProductCsvRecord>().ToList(); foreach (var record in records) { yield return new object[] { record.ProductId, record.Name, record.Category, record.Price, record.IsAvailable }; } } [Theory] [MemberAutoData(nameof(GetProductsFromCsv))] public void CSV整合測試_產品驗證( int productId, string productName, string category, decimal price, bool isAvailable, Customer customer, Order order) { // Assert - CSV 資料 productId.Should().BePositive(); productName.Should().NotBeNullOrEmpty(); category.Should().BeOneOf("3C產品", "運動用品"); price.Should().BePositive(); // Assert - AutoFixture 產生的資料 customer.Should().NotBeNull(); order.Should().NotBeNull(); } } public class ProductCsvRecord { public int ProductId { get; set; } public string Name { get; set; } = string.Empty; public string Category { get; set; } = string.Empty; public decimal Price { get; set; } public bool IsAvailable { get; set; } }
JSON 檔案整合
TestData/customers.json
[ { "customerId": 1, "name": "張三", "email": "zhang@example.com", "type": "VIP", "creditLimit": 100000 }, { "customerId": 2, "name": "李四", "email": "li@example.com", "type": "Premium", "creditLimit": 50000 } ]
JSON 讀取與整合
using System.Text.Json; public static IEnumerable<object[]> GetCustomersFromJson() { var jsonPath = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "customers.json"); var jsonContent = File.ReadAllText(jsonPath); var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var customers = JsonSerializer.Deserialize<List<CustomerJsonRecord>>(jsonContent, options)!; foreach (var customer in customers) { yield return new object[] { customer.CustomerId, customer.Name, customer.Email, customer.Type, customer.CreditLimit }; } } [Theory] [MemberAutoData(nameof(GetCustomersFromJson))] public void JSON整合測試_客戶驗證( int customerId, string name, string email, string customerType, decimal creditLimit, Order order) { // Assert - JSON 資料 customerId.Should().BePositive(); name.Should().NotBeNullOrEmpty(); email.Should().Contain("@"); customerType.Should().BeOneOf("VIP", "Premium", "Regular"); creditLimit.Should().BePositive(); // Assert - AutoFixture 產生的資料 order.Should().NotBeNull(); }
資料來源設計模式
階層式資料組織
namespace AutoData.Tests.DataSources; /// <summary> /// 測試資料來源基底類別 /// </summary> public abstract class BaseTestData { protected static string GetTestDataPath(string fileName) { return Path.Combine(Directory.GetCurrentDirectory(), "TestData", fileName); } } /// <summary> /// 產品測試資料來源 /// </summary> public class ProductTestDataSource : BaseTestData { public static IEnumerable<object[]> BasicProducts() { yield return new object[] { "iPhone", 35900m, true }; yield return new object[] { "MacBook", 89900m, true }; yield return new object[] { "AirPods", 7490m, false }; } public static IEnumerable<object[]> ElectronicsProducts() { // 從 CSV 檔案讀取 var csvPath = GetTestDataPath("electronics.csv"); // ... 讀取邏輯 } } /// <summary> /// 客戶測試資料來源 /// </summary> public class CustomerTestDataSource : BaseTestData { public static IEnumerable<object[]> VipCustomers() { yield return new object[] { "張三", "VIP", 100000m }; yield return new object[] { "李四", "VIP", 150000m }; } }
可重用資料集
/// <summary> /// 可重用的測試資料集 /// </summary> public static class ReusableTestDataSets { public static class ProductCategories { public static IEnumerable<object[]> All() { yield return new object[] { "3C產品", "TECH" }; yield return new object[] { "服飾配件", "FASHION" }; yield return new object[] { "居家生活", "HOME" }; } public static IEnumerable<object[]> Electronics() { yield return new object[] { "手機", "MOBILE" }; yield return new object[] { "筆電", "LAPTOP" }; } } public static class CustomerTypes { public static IEnumerable<object[]> All() { yield return new object[] { "VIP", 100000m, 0.15m }; yield return new object[] { "Premium", 50000m, 0.10m }; yield return new object[] { "Regular", 20000m, 0.05m }; } } }
與 Awesome Assertions 協作
[Theory] [InlineAutoData("VIP", 100000)] [InlineAutoData("Premium", 50000)] [InlineAutoData("Regular", 20000)] public void AutoData與AwesomeAssertions協作_客戶等級驗證( string customerLevel, decimal expectedCreditLimit, [Range(1000, 15000)] decimal orderAmount, Customer customer, Order order) { // Arrange customer.Type = customerLevel; customer.CreditLimit = expectedCreditLimit; order.Amount = orderAmount; // Act var canPlaceOrder = customer.CanPlaceOrder(order.Amount); var discountRate = CalculateDiscount(customer.Type, order.Amount); // Assert - 使用 Awesome Assertions 語法 customer.Type.Should().Be(customerLevel); customer.CreditLimit.Should().Be(expectedCreditLimit); customer.CreditLimit.Should().BePositive(); order.Amount.Should().BeInRange(1000m, 15000m); canPlaceOrder.Should().BeTrue(); discountRate.Should().BeInRange(0m, 0.3m); } private static decimal CalculateDiscount(string customerType, decimal orderAmount) { var baseDiscount = customerType switch { "VIP" => 0.15m, "Premium" => 0.10m, "Regular" => 0.05m, _ => 0m }; var largeOrderBonus = orderAmount > 30000m ? 0.05m : 0m; return Math.Min(baseDiscount + largeOrderBonus, 0.3m); }
最佳實踐
應該做
-
善用 DataAnnotation
- 在參數上使用
、[StringLength]
約束資料範圍[Range] - 確保產生的資料符合業務規則
- 在參數上使用
-
建立可重用的自訂 AutoData
- 為不同領域建立專屬的 AutoData 屬性
- 集中管理測試資料的產生規則
-
使用 MemberAutoData 處理複雜資料
- 當 InlineAutoData 無法滿足需求時使用
- 支援變數、運算式和外部資料來源
-
組織測試資料來源
- 建立階層式的資料來源結構
- 將相關資料集中管理
應該避免
-
在 InlineAutoData 使用非常數值
- InlineAutoData 只接受編譯時常數
- 需要動態值時改用 MemberAutoData
-
過度複雜的 CompositeAutoData
- 避免組合過多的 AutoData 來源
- 保持配置的可理解性
-
忽略參數順序
- InlineAutoData 的固定值必須與參數順序一致
- 錯誤的順序會導致型別不符
程式碼範本
請參考 templates 資料夾中的範例檔案:
- autodata-attributes.cs - AutoData 屬性家族使用範例
- external-data-integration.cs - CSV/JSON 外部資料整合
- advanced-patterns.cs - 進階模式與 CollectionSizeAttribute
與其他技能的關係
- autofixture-basics:本技能的前置知識
- autofixture-customization:自訂化策略可用於 AutoData 屬性
- autofixture-nsubstitute-integration:下一步學習目標
- awesome-assertions-guide:搭配使用提升測試可讀性
參考資源
原始文章
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
- Day 12 - 結合 AutoData:xUnit 與 AutoFixture 的整合應用