dotnet-testing-datetime-testing-timeprovider
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-datetime-testing-timeprovider
Agent 安装分布
Skill 文档
DateTime èæéç¸ä¾æ§æ¸¬è©¦æå
é©ç¨æ å¢
æ¬æè½æå°å¦ä½ä½¿ç¨ Microsoft.Bcl.TimeProvider 解決æéç¸ä¾ç¨å¼ç¢¼ç測試åé¡ãééæéæ½è±¡åï¼è®ãç¾å¨æéãè®å¾å¯æ§å¶ãå¯é 測ãå¯éç¾ã
é©ç¨å ´æ¯
- çæ¥æé夿·ï¼ç³»çµ±æ ¹æç¶åæéæ±ºå®æ¯å¦å 許æä½
- åªæ æ´»åæ§å¶ï¼ç¹å®æ¥ææææ®µæçæçä¿é·é輯
- å¿«åéææ©å¶ï¼ä¾ææé決å®è³ææ¯å¦ææ
- æç¨ä»»å觸ç¼ï¼å®æå·è¡çèæ¯ä½æ¥
- Token æææéï¼é©èæéææçå®å ¨æ©å¶
å¿ è¦å¥ä»¶
<!-- æ£å¼ç¨å¼ç¢¼ -->
<PackageReference Include="Microsoft.Bcl.TimeProvider" Version="9.0.0" />
<!-- æ¸¬è©¦å°æ¡ -->
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
æ ¸å¿åå
ååä¸ï¼æéæ½è±¡å – 以 TimeProvider å代 DateTime
å³çµ±åé¡ç¨å¼ç¢¼ï¼
// â ç¡æ³æ¸¬è©¦ - ç´æ¥ä½¿ç¨éæ
æé
public class OrderService
{
public bool CanPlaceOrder()
{
var now = DateTime.Now;
return now.Hour >= 9 && now.Hour < 17;
}
}
坿¸¬è©¦çéæ§ï¼
// â
坿¸¬è©¦ - ééä¾è³´æ³¨å
¥æ¥æ¶ TimeProvider
public class OrderService
{
private readonly TimeProvider _timeProvider;
public OrderService(TimeProvider timeProvider)
{
_timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
}
public bool CanPlaceOrder()
{
var now = _timeProvider.GetLocalNow();
return now.Hour >= 9 && now.Hour < 17;
}
}
ä¾è³´æ³¨å ¥è¨å®ï¼
// Program.cs - çç¢ç°å¢ä½¿ç¨ç³»çµ±æé
services.AddSingleton(TimeProvider.System);
services.AddScoped<OrderService>();
ååäºï¼FakeTimeProvider æ§å¶æ¸¬è©¦æé
FakeTimeProvider æä¾å®æ´çæéæ§å¶è½åï¼
| æ¹æ³ | ç¨é | ä½¿ç¨ææ© |
|---|---|---|
SetUtcNow(DateTimeOffset) |
è¨å® UTC æé | éè¦ç²¾ç¢º UTC æéæ |
SetLocalTimeZone(TimeZoneInfo) |
è¨å®æ¬å°æå | 測試æåç¸éé輯 |
Advance(TimeSpan) |
æéå¿«è½ | 測試éæãå»¶é²é輯 |
GetUtcNow() |
åå¾ UTC æé | è®åç¶åæ¨¡æ¬æé |
GetLocalNow() |
å徿¬å°æé | è®åæ¬å°æ¨¡æ¬æé |
å»ºè°æ´å æ¹æ³ï¼
public static class FakeTimeProviderExtensions
{
/// <summary>
/// è¨å® FakeTimeProvider çæ¬å°æé
/// </summary>
public static void SetLocalNow(this FakeTimeProvider fakeTimeProvider, DateTime localDateTime)
{
fakeTimeProvider.SetLocalTimeZone(TimeZoneInfo.Local);
var utcTime = TimeZoneInfo.ConvertTimeToUtc(localDateTime, TimeZoneInfo.Local);
fakeTimeProvider.SetUtcNow(utcTime);
}
}
ååä¸ï¼æ¯å測試使ç¨ç¨ç«çæéç°å¢
// â
æ£ç¢ºï¼æ¯å測試ç¨ç«å»ºç« FakeTimeProvider
public class OrderServiceTests
{
[Fact]
public void CanPlaceOrder_å¨çæ¥æéå
§_æåå³True()
{
// Arrange - ç¨ç«å¯¦ä¾
var fakeTimeProvider = new FakeTimeProvider();
fakeTimeProvider.SetLocalNow(new DateTime(2024, 3, 15, 14, 0, 0));
var sut = new OrderService(fakeTimeProvider);
// Act
var result = sut.CanPlaceOrder();
// Assert
result.Should().BeTrue();
}
}
// â é¿å
ï¼å¤å測試å
±ç¨éæ
實ä¾
public class BadTestClass
{
private static readonly FakeTimeProvider SharedProvider = new(); // æäºç¸å¹²æ¾
}
é²éæéæ§å¶æè¡
æéåçµ
ç¶éè¦é©èå¤åæä½ç¼çå¨ãå䏿éé»ãï¼
[Fact]
public void ProcessBatch_å¨åºå®æéé»_æç¢çç¸åæéæ³()
{
var fakeTimeProvider = new FakeTimeProvider();
var fixedTime = new DateTime(2024, 12, 25, 10, 30, 0);
fakeTimeProvider.SetLocalNow(fixedTime);
var processor = new BatchProcessor(fakeTimeProvider);
var result1 = processor.ProcessItem("Item1");
var result2 = processor.ProcessItem("Item2");
// æé被åçµï¼å
©æ¬¡æä½çæéæ³ç¸å
result1.Timestamp.Should().Be(result2.Timestamp);
}
æéå¿«è½ (Advance)
測試快åéæãToken 失æçæéææé輯ï¼
[Fact]
public void Cache_ç¶ééææé_ææ¸
é¤é
ç®()
{
var fakeTimeProvider = new FakeTimeProvider();
fakeTimeProvider.SetLocalNow(new DateTime(2024, 3, 15, 10, 0, 0));
var cache = new TimedCache(fakeTimeProvider, TimeSpan.FromMinutes(5));
cache.Set("key", "value");
// 3 åéå¾ - å°æªéæ
fakeTimeProvider.Advance(TimeSpan.FromMinutes(3));
cache.Get("key").Should().Be("value");
// å 3 åéå¾ï¼å
± 6 åéï¼- å·²éæ
fakeTimeProvider.Advance(TimeSpan.FromMinutes(3));
cache.Get("key").Should().BeNull();
}
éè¦ï¼
Advance()æ¯éé»å¡çï¼ç¬é宿æéè·³èºï¼ä¸æçæ£çå¾ ã
æéåè½
測試æ·å²è³æèçæéæå ´æ¯ï¼
[Fact]
public void HistoricalDataProcessor_åå°é廿é_ææ£ç¢ºèç()
{
var fakeTimeProvider = new FakeTimeProvider();
var historicalTime = new DateTime(2020, 1, 15, 9, 0, 0);
fakeTimeProvider.SetLocalNow(historicalTime);
var processor = new HistoricalDataProcessor(fakeTimeProvider);
var result = processor.ProcessDataForDate(historicalTime.Date);
result.ProcessedAt.Should().Be(historicalTime);
}
å¯¦æ°æ¸¬è©¦æ¨¡å¼
模å¼ä¸ï¼åæ¸åéçæ¸¬è©¦
[Theory]
[InlineData(8, false)] // ä¸å 8 é» - çæ¥æéå
[InlineData(9, true)] // ä¸å 9 é» - åéå§çæ¥
[InlineData(12, true)] // ä¸å 12 é» - çæ¥æéå
§
[InlineData(16, true)] // ä¸å 4 é» - çæ¥æéå
§
[InlineData(17, false)] // ä¸å 5 é» - åçµæçæ¥
[InlineData(18, false)] // ä¸å 6 é» - çæ¥æéå¾
public void CanPlaceOrder_ä¸åæéé»_æå峿£ç¢ºçµæ(int hour, bool expected)
{
var fakeTimeProvider = new FakeTimeProvider();
fakeTimeProvider.SetLocalNow(new DateTime(2024, 3, 15, hour, 0, 0));
var sut = new OrderService(fakeTimeProvider);
sut.CanPlaceOrder().Should().Be(expected);
}
模å¼äºï¼äº¤ææéçªå£æ¸¬è©¦
[Theory]
[InlineData("09:30:00", true)] // ä¸å交ææé
[InlineData("12:00:00", false)] // ä¸å伿¯
[InlineData("14:30:00", true)] // ä¸å交ææé
[InlineData("15:30:00", false)] // 交æçµæå¾
public void IsInTradingHours_ä¸åæé_æå峿£ç¢ºçµæ(string timeStr, bool expected)
{
var fakeTimeProvider = new FakeTimeProvider();
var testTime = DateTime.Today.Add(TimeSpan.Parse(timeStr));
fakeTimeProvider.SetLocalNow(testTime);
var sut = new TradingService(fakeTimeProvider);
sut.IsInTradingHours().Should().Be(expected);
}
模å¼ä¸ï¼æç¨è§¸ç¼é輯測試
[Theory]
[InlineData("2024-03-15 14:30:00", "2024-03-15 14:00:00", true)] // å·²å°å·è¡æé
[InlineData("2024-03-15 13:30:00", "2024-03-15 14:00:00", false)] // å°æªå°æé
public void ShouldExecuteJob_æ ¹ææé夿·_æå峿£ç¢ºçµæ(
string currentTimeStr, string scheduledTimeStr, bool expected)
{
var fakeTimeProvider = new FakeTimeProvider();
fakeTimeProvider.SetLocalNow(DateTime.Parse(currentTimeStr));
var schedule = new JobSchedule { NextExecutionTime = DateTime.Parse(scheduledTimeStr) };
var sut = new ScheduleService(fakeTimeProvider);
sut.ShouldExecuteJob(schedule).Should().Be(expected);
}
AutoFixture æ´å
FakeTimeProviderCustomization
public class FakeTimeProviderCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Register(() => new FakeTimeProvider());
}
}
AutoDataWithCustomization 屬æ§
public class AutoDataWithCustomizationAttribute : AutoDataAttribute
{
public AutoDataWithCustomizationAttribute() : base(CreateFixture)
{
}
private static IFixture CreateFixture()
{
return new Fixture()
.Customize(new AutoNSubstituteCustomization())
.Customize(new FakeTimeProviderCustomization());
}
}
ä½¿ç¨ Matching.DirectBaseType
[Theory]
[AutoDataWithCustomization]
public void GetTimeBasedDiscount_é±äº_æåå³ä¹æåªæ (
[Frozen(Matching.DirectBaseType)] FakeTimeProvider fakeTimeProvider,
OrderService sut)
{
// Matching.DirectBaseType è® AutoFixture ç¥éï¼
// ç¶éè¦ TimeProviderï¼åºåºé¡åï¼æï¼ä½¿ç¨ FakeTimeProviderï¼è¡çé¡åï¼
var fridayTime = new DateTime(2024, 3, 15, 14, 0, 0); // é±äº
fakeTimeProvider.SetLocalNow(fridayTime);
sut.GetTimeBasedDiscount().Should().Be("é±äºå¿«æ¨ï¼ä¹æåªæ ");
}
ééµï¼å¿ é 使ç¨
[Frozen(Matching.DirectBaseType)]ï¼å¦å AutoFixture ç¡æ³æ£ç¢ºå° FakeTimeProvider æ³¨å ¥å°éè¦ TimeProvider ç建æ§å¼ä¸ã
æä½³å¯¦è¸æª¢æ¥æ¸ å®
â ç¨å¼ç¢¼è¨è¨
- æææéç¸ä¾é¡å¥éé建æ§å¼æ¥æ¶
TimeProvider - 使ç¨
_timeProvider.GetLocalNow()å代DateTime.Now - 使ç¨
_timeProvider.GetUtcNow()å代DateTime.UtcNow - DI 容å¨è¨»å
TimeProvider.Systemä½çºçç¢ç°å¢å¯¦ä½
â æ¸¬è©¦è¨è¨
- æ¯åæ¸¬è©¦æ¹æ³ä½¿ç¨ç¨ç«ç
FakeTimeProviderå¯¦ä¾ - 使ç¨
SetLocalNow()æ´å æ¹æ³ç°¡åæéè¨å® - 使ç¨
Advance()測試æéææé輯ï¼å¿«åãéæãå»¶é²ï¼ - 測試涵èéçæ¢ä»¶ï¼éå§æéãçµææéãè¨çé»ï¼
â é²éèé
- FakeTimeProvider æ¯å·è¡ç·å®å ¨çï¼å¯ç¨æ¼ä¸¦è¡æ¸¬è©¦
- 使ç¨
IDisposableæ¨¡å¼æ£ç¢ºéæ¾ FakeTimeProvider - æå測試使ç¨
SetLocalTimeZone()æç¢ºè¨å®æå
åèè³æº
åå§æç«
æ¬æè½å §å®¹æç èªãèæ´¾è»é«å·¥ç¨å¸«ç測試修練 – 30 å¤©ææ°ãç³»åæç« ï¼
- Day 16 – æ¸¬è©¦æ¥æèæéï¼Microsoft.Bcl.TimeProvider å代 DateTime
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10375821
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day16
宿¹æä»¶
ç¸éæè½
autofixture-basics– AutoFixture èªåæ¸¬è©¦è³æçænsubstitute-mocking– 測試æ¿èº«è模æ¬autodata-xunit-integration– xUnit è AutoFixture ç AutoData æ´å