xiaohongshu
xiaohongshu
dotnet-testing-autodata-xunit-integration
AutoFixture 與 xUnit 整合完整指南。涵蓋 AutoData、InlineAutoData、自訂 Customization 與測試資料屬性。簡化參數化測試資料準備,提升測試可讀性與維護性。 Keywords: AutoData, InlineAutoData, AutoFixture xUnit, [AutoData], [InlineAutoData], AutoDataAttribute, ICustomization, DataAttribute, 參數化測試, Theory AutoData, MemberAutoData, 測試資料屬性, fixture.Cu...
Full skill instructions
AutoData 屬性家族是 AutoFixture.Xunit2 套件提供的功能,將 AutoFixture 的資料產生能力與 xUnit 的參數化測試整合,讓測試參數自動注入,大幅減少測試準備程式碼。
<PackageReference Include="AutoFixture" Version="4.18.1" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
dotnet add package AutoFixture.Xunit2
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);
}
[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 結合了 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 允許從類別的方法、屬性或欄位中獲取測試資料。
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 配置:
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;
}
}
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;
}
}
[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");
}
組合多個自訂 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);
}
AutoData 預設的集合大小是 3,可透過自訂屬性控制:
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)));
}
}
}
[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>
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; }
}
TestData/customers.json
[
{
"customerId": 1,
"name": "張三",
"email": "[email protected]",
"type": "VIP",
"creditLimit": 100000
},
{
"customerId": 2,
"name": "李四",
"email": "[email protected]",
"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 };
}
}
}
[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
使用 MemberAutoData 處理複雜資料
組織測試資料來源
在 InlineAutoData 使用非常數值
過度複雜的 CompositeAutoData
忽略參數順序
請參考 templates 資料夾中的範例檔案:
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience