dotnet-testing-autofixture-customization
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-autofixture-customization
Agent 安装分布
Skill 文档
AutoFixture é²é:èªè¨åæ¸¬è©¦è³æçæçç¥
觸ç¼ééµå
- autofixture customization
- autofixture customize
- ISpecimenBuilder
- specimen builder
- DataAnnotations autofixture
- 屬æ§ç¯åæ§å¶
- fixture.Customizations
- Insert(0)
- RandomDateTimeSequenceGenerator
- NumericRangeBuilder
- èªè¨å»ºæ§å¨
- custom builder autofixture
æ¦è¿°
æ¬æè½æ¶µè AutoFixture çé²éèªè¨ååè½ï¼è®æ¨è½æ ¹ææ¥åéæ±ç²¾ç¢ºæ§å¶æ¸¬è©¦è³æççæé輯ãå¾ DataAnnotations èªåæ´åå°èªè¨ ISpecimenBuilder 實ä½ï¼ææ¡éäºæè¡è½è®æ¸¬è©¦è³ææ´ç¬¦å坦鿥åéæ±ã
æ ¸å¿æè¡
- DataAnnotations æ´åï¼AutoFixture èªåèå¥
[StringLength]ã[Range]çé©èå±¬æ§ - 屬æ§ç¯åæ§å¶ï¼ä½¿ç¨
.With()é åRandom.Sharedåæ ç¢ç鍿©å¼ - èªè¨ ISpecimenBuilderï¼å¯¦ä½ç²¾ç¢ºæ§å¶ç¹å®å±¬æ§ç建æ§å¨
- åªå
é åºç®¡çï¼çè§£
Insert(0)vsAdd()çå·®ç° - æ³ååè¨è¨ï¼å»ºç«æ¯æ´å¤ç¨®æ¸å¼åå¥çå¯éç¨å»ºæ§å¨
å®è£å¥ä»¶
<PackageReference Include="AutoFixture" Version="4.18.1" />
<PackageReference Include="AutoFixture.Xunit2" Version="4.18.1" />
DataAnnotations èªåæ´å
AutoFixture è½èªåèå¥ System.ComponentModel.DataAnnotations çé©è屬æ§ï¼
using System.ComponentModel.DataAnnotations;
public class Person
{
public Guid Id { get; set; }
[StringLength(10)]
public string Name { get; set; } = string.Empty;
[Range(10, 80)]
public int Age { get; set; }
public DateTime CreateTime { get; set; }
}
[Fact]
public void AutoFixture_æè½èå¥DataAnnotations()
{
var fixture = new Fixture();
var person = fixture.Create<Person>();
person.Name.Length.Should().Be(10); // StringLength(10)
person.Age.Should().BeInRange(10, 80); // Range(10, 80)
}
[Fact]
public void AutoFixture_æ¹éç¢ç_é½ç¬¦åéå¶()
{
var fixture = new Fixture();
var persons = fixture.CreateMany<Person>(10).ToList();
persons.Should().AllSatisfy(person =>
{
person.Name.Length.Should().Be(10);
person.Age.Should().BeInRange(10, 80);
});
}
ä½¿ç¨ .With() æ§å¶å±¬æ§ç¯å
åºå®å¼ vs åæ å¼
// â åºå®å¼ï¼åªå·è¡ä¸æ¬¡ï¼ææç©ä»¶ç¸åå¼
.With(x => x.Age, Random.Shared.Next(30, 50))
// â
åæ
å¼ï¼æ¯åç©ä»¶é½éæ°è¨ç®
.With(x => x.Age, () => Random.Shared.Next(30, 50))
宿´ç¯ä¾
[Fact]
public void Withæ¹æ³_åºå®å¼vsåæ
å¼çå·®ç°()
{
var fixture = new Fixture();
// åºå®å¼ï¼ææç©ä»¶å¹´é½¡ç¸å
var fixedAgeMembers = fixture.Build<Member>()
.With(x => x.Age, Random.Shared.Next(30, 50))
.CreateMany(5)
.ToList();
// åæ
å¼ï¼æ¯åç©ä»¶å¹´é½¡ä¸å
var dynamicAgeMembers = fixture.Build<Member>()
.With(x => x.Age, () => Random.Shared.Next(30, 50))
.CreateMany(5)
.ToList();
// åºå®å¼ï¼åªæä¸ç¨®å¹´é½¡
fixedAgeMembers.Select(m => m.Age).Distinct().Count().Should().Be(1);
// åæ
å¼ï¼é常æå¤ç¨®å¹´é½¡
dynamicAgeMembers.Select(m => m.Age).Distinct().Count().Should().BeGreaterThan(1);
}
Random.Shared çåªé»
| ç¹æ§ | new Random() |
Random.Shared |
|---|---|---|
| 實ä¾åæ¹å¼ | æ¯æ¬¡å»ºç«æ°å¯¦ä¾ | å ¨åå ±ç¨å®ä¸å¯¦ä¾ |
| å·è¡ç·å®å ¨ | â 䏿¯ | â æ¯ |
| æè½ | 夿¬¡å»ºç«æè² æï¼å¯è½éè¤å¼ | æè½æ´ä½³ï¼é¿å éè¤å¼ |
| ç¨éå»ºè° | å®å·è¡ç·ãçæç¨é | å¤å·è¡ç·ãå ¨åå ±ç¨ |
èªè¨ ISpecimenBuilder
RandomRangedDateTimeBuilderï¼ç²¾ç¢ºæ§å¶ DateTime 屬æ§
RandomDateTimeSequenceGenerator æå½±é¿ææ DateTime 屬æ§ãè¥éæ§å¶ç¹å®å±¬æ§ï¼éèªè¨å»ºæ§å¨ï¼
using AutoFixture.Kernel;
using System.Reflection;
public class RandomRangedDateTimeBuilder : ISpecimenBuilder
{
private readonly DateTime _minDate;
private readonly DateTime _maxDate;
private readonly HashSet<string> _targetProperties;
public RandomRangedDateTimeBuilder(
DateTime minDate,
DateTime maxDate,
params string[] targetProperties)
{
_minDate = minDate;
_maxDate = maxDate;
_targetProperties = new HashSet<string>(targetProperties);
}
public object Create(object request, ISpecimenContext context)
{
if (request is PropertyInfo propertyInfo &&
propertyInfo.PropertyType == typeof(DateTime) &&
_targetProperties.Contains(propertyInfo.Name))
{
var range = _maxDate - _minDate;
var randomTicks = (long)(Random.Shared.NextDouble() * range.Ticks);
return _minDate.AddTicks(randomTicks);
}
return new NoSpecimen();
}
}
使ç¨ç¯ä¾
[Fact]
public void åªæ§å¶ç¹å®DateTime屬æ§()
{
var fixture = new Fixture();
var minDate = new DateTime(2025, 1, 1);
var maxDate = new DateTime(2025, 12, 31);
// åªæ§å¶ UpdateTime 屬æ§
fixture.Customizations.Add(
new RandomRangedDateTimeBuilder(minDate, maxDate, "UpdateTime"));
var member = fixture.Create<Member>();
// UpdateTime 卿å®ç¯å
member.UpdateTime.Should().BeOnOrAfter(minDate).And.BeOnOrBefore(maxDate);
// CreateTime ä¸åå½±é¿
}
NoSpecimen çéè¦æ§
NoSpecimen 表示æ¤å»ºæ§å¨ç¡æ³èçè«æ±ï¼äº¤ç±è²¬ä»»éä¸ä¸ä¸å建æ§å¨èçï¼
public object Create(object request, ISpecimenContext context)
{
// 䏿¯æåçç®æ¨ â åå³ NoSpecimen
if (request is not PropertyInfo propertyInfo)
return new NoSpecimen();
if (propertyInfo.PropertyType != typeof(DateTime))
return new NoSpecimen();
if (!_targetProperties.Contains(propertyInfo.Name))
return new NoSpecimen();
// æ¯æåçç®æ¨ â ç¢çå¼
return GenerateRandomDateTime();
}
åªå é åºç®¡çï¼Insert(0) vs Add()
åé¡ï¼å §å»ºå»ºæ§å¨åªå é åºæ´é«
AutoFixture å
§å»ºç RangeAttributeRelayãNumericSequenceGenerator å¯è½æ¯èªè¨å»ºæ§å¨ææ´é«åªå
é åºï¼
// â å¯è½å¤±æï¼è¢«å
§å»ºå»ºæ§å¨ææª
fixture.Customizations.Add(new MyNumericBuilder(30, 50, "Age"));
// â
æ£ç¢ºï¼ç¢ºä¿æé«åªå
é åº
fixture.Customizations.Insert(0, new MyNumericBuilder(30, 50, "Age"));
æ¹é²çæ¸å¼ç¯å建æ§å¨
public class ImprovedRandomRangedNumericSequenceBuilder : ISpecimenBuilder
{
private readonly int _min;
private readonly int _max;
private readonly Func<PropertyInfo, bool> _predicate;
public ImprovedRandomRangedNumericSequenceBuilder(
int min,
int max,
Func<PropertyInfo, bool> predicate)
{
_min = min;
_max = max;
_predicate = predicate;
}
public object Create(object request, ISpecimenContext context)
{
if (request is PropertyInfo propertyInfo &&
propertyInfo.PropertyType == typeof(int) &&
_predicate(propertyInfo))
{
return Random.Shared.Next(_min, _max);
}
return new NoSpecimen();
}
}
ä½¿ç¨ Insert(0) 確ä¿åªå é åº
[Fact]
public void 使ç¨Insert0確ä¿åªå
é åº()
{
var fixture = new Fixture();
// ä½¿ç¨ Insert(0) ç¢ºä¿æé«åªå
é åº
fixture.Customizations.Insert(0,
new ImprovedRandomRangedNumericSequenceBuilder(
30, 50,
prop => prop.Name == "Age" && prop.DeclaringType == typeof(Member)));
var members = fixture.CreateMany<Member>(20).ToList();
members.Should().AllSatisfy(m => m.Age.Should().BeInRange(30, 49));
}
æ³å忏å¼ç¯å建æ§å¨
NumericRangeBuilder
public class NumericRangeBuilder<TValue> : ISpecimenBuilder
where TValue : struct, IComparable, IConvertible
{
private readonly TValue _min;
private readonly TValue _max;
private readonly Func<PropertyInfo, bool> _predicate;
public NumericRangeBuilder(
TValue min,
TValue max,
Func<PropertyInfo, bool> predicate)
{
_min = min;
_max = max;
_predicate = predicate;
}
public object Create(object request, ISpecimenContext context)
{
if (request is PropertyInfo propertyInfo &&
propertyInfo.PropertyType == typeof(TValue) &&
_predicate(propertyInfo))
{
return GenerateRandomValue();
}
return new NoSpecimen();
}
private TValue GenerateRandomValue()
{
var minDecimal = Convert.ToDecimal(_min);
var maxDecimal = Convert.ToDecimal(_max);
var range = maxDecimal - minDecimal;
var randomValue = minDecimal + (decimal)Random.Shared.NextDouble() * range;
return typeof(TValue).Name switch
{
nameof(Int32) => (TValue)(object)(int)randomValue,
nameof(Int64) => (TValue)(object)(long)randomValue,
nameof(Int16) => (TValue)(object)(short)randomValue,
nameof(Byte) => (TValue)(object)(byte)randomValue,
nameof(Single) => (TValue)(object)(float)randomValue,
nameof(Double) => (TValue)(object)(double)randomValue,
nameof(Decimal) => (TValue)(object)randomValue,
_ => throw new NotSupportedException($"Type {typeof(TValue).Name} is not supported")
};
}
}
æµæ¢ä»é¢æ´å æ¹æ³
public static class FixtureRangedNumericExtensions
{
public static IFixture AddRandomRange<T, TValue>(
this IFixture fixture,
TValue min,
TValue max,
Func<PropertyInfo, bool> predicate)
where TValue : struct, IComparable, IConvertible
{
fixture.Customizations.Insert(0,
new NumericRangeBuilder<TValue>(min, max, predicate));
return fixture;
}
}
宿´ä½¿ç¨ç¯ä¾
public class Product
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Quantity { get; set; }
public double Rating { get; set; }
public float Discount { get; set; }
}
[Fact]
public void å¤éæ¸å¼åå¥ç¯åæ§å¶()
{
var fixture = new Fixture();
fixture
.AddRandomRange<Product, decimal>(
50m, 500m,
prop => prop.Name == "Price" && prop.DeclaringType == typeof(Product))
.AddRandomRange<Product, int>(
1, 50,
prop => prop.Name == "Quantity" && prop.DeclaringType == typeof(Product))
.AddRandomRange<Product, double>(
1.0, 5.0,
prop => prop.Name == "Rating" && prop.DeclaringType == typeof(Product))
.AddRandomRange<Product, float>(
0.0f, 0.5f,
prop => prop.Name == "Discount" && prop.DeclaringType == typeof(Product));
var products = fixture.CreateMany<Product>(10).ToList();
products.Should().AllSatisfy(product =>
{
product.Price.Should().BeInRange(50m, 500m);
product.Quantity.Should().BeInRange(1, 49);
product.Rating.Should().BeInRange(1.0, 5.0);
product.Discount.Should().BeInRange(0.0f, 0.5f);
});
}
int vs DateTime èçå·®ç°
çºä½ DateTime 建æ§å¨ç¨ Add() å°±è½çæï¼
| åå¥ | å §å»ºå»ºæ§å¨ | åªå é åºå½±é¿ |
|---|---|---|
int |
RangeAttributeRelayãNumericSequenceGenerator |
æè¢«ææªï¼éç¨ Insert(0) |
DateTime |
ç¡ç¹å®å»ºæ§å¨ | ä¸æè¢«ææªï¼Add() å³å¯ |
æä½³å¯¦è¸
æè©²å
-
åç¨ DataAnnotations
- å åå©ç¨ç¾ææ¨¡åé©èè¦å
- AutoFixture èªåç¢ç符åéå¶çè³æ
-
ä½¿ç¨ Random.Shared
- é¿å éè¤å¼åé¡
- å·è¡ç·å®å ¨ãæè½æ´å¥½
-
Insert(0) 確ä¿åªå é åº
- èªè¨æ¸å¼å»ºæ§å¨åå¿
ç¨
Insert(0) - é¿å è¢«å §å»ºå»ºæ§å¨è¦è
- èªè¨æ¸å¼å»ºæ§å¨åå¿
ç¨
-
æ³ååè¨è¨
- 建ç«å¯éç¨çæ³å建æ§å¨
- ä½¿ç¨æ´å æ¹æ³æä¾æµæ¢ä»é¢
æè©²é¿å
-
忽ç¥å»ºæ§å¨åªå é åº
- ä¸è¦åè¨
Add()ä¸å®çæ - 測試é©è建æ§å¨æ¯å¦æ£å¸¸éä½
- ä¸è¦åè¨
-
é度è¤éçé輯
- 建æ§å¨ä¿æå®ä¸è·è²¬
- è¤éæ¥åé輯æ¾å¨æ¸¬è©¦ææå層
-
ä½¿ç¨ new Random()
- å¯è½ç¢çéè¤å¼
- éå·è¡ç·å®å ¨
ç¨å¼ç¢¼ç¯æ¬
è«åè templates è³æå¤¾ä¸çç¯ä¾æªæ¡ï¼
- dataannotations-integration.cs – DataAnnotations èªåæ´å
- custom-specimen-builders.cs – èªè¨ ISpecimenBuilder 實ä½
- numeric-range-extensions.cs – æ³å忏å¼ç¯å建æ§å¨èæ´å æ¹æ³
èå ¶ä»æè½çéä¿
- autofixture-basicsï¼æ¬æè½çåç½®ç¥èï¼éå ææ¡åºç¤ç¨æ³
- autodata-xunit-integrationï¼ä¸ä¸æ¥å¸ç¿ç®æ¨ï¼å°èªè¨åè xUnit æ´å
- autofixture-nsubstitute-integrationï¼é²éæ´åï¼çµå Mock èèªè¨è³æçæ
åèè³æº
åå§æç«
æ¬æè½å §å®¹æç èªãèæ´¾è»é«å·¥ç¨å¸«ç測試修練 – 30 å¤©ææ°ãç³»åæç« ï¼
- Day 11 – AutoFixture é²éï¼èªè¨åæ¸¬è©¦è³æçæçç¥
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10375153
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day11