Skip to content

Commit 7c6b256

Browse files
authored
fix: MockTimeProvider constructor treats "now" having DateTimeKind.Unspecified as if it had DateTimeKind.Local (#834)
The difference between `DateTime.Now` And `DateTime.UtcNow` should be the offset from the local time zone to UTC. Before this change, `MockTimeSystem` would produce a difference double that size when initialized with a `DateTime` having `Kind` `DateTimeKind.Unspecified`. The `MockTimeSystem` delegates to a `MockDateTime` wrapped around a `TimeProviderMock`. The `TimeProviderMock` would yield a `DateTime` with `Kind` `DateTimeKind.Unspecified` to the `MockDateTime`, which would then apply `ToLocalTime()` for `Now` or `ToUniversalTime` for `UtcNow`. When applied to a `DateTime` with `Kind` `DateTimeKind.Unspecified`, `ToLocalTime` assumes that the value is in UTC, but `ToUniversalTime` assumes that it is Local. This discrepancy results in the doubling of the expected difference. Additionally, the test `OnDateTimeRead_Today_ShouldExecuteCallbackWithCorrectParameter` was failing, but only when run on systems outside of UTC. On systems running inside UTC, there was no difference between `Now` and `UtcNow`, so the error was hidden. So, when the `MockTimeSystem` is initialized with a `DateTime` which has `Kind` `DateTimeKind.Unspecified`, it should pick a specific kind to use internally. Either `Utc` or `Local` would work. `Utc` was selected because local times often lead to tests that only work in a specific time zone and should be selected intentionally.
1 parent d172459 commit 7c6b256

File tree

5 files changed

+34
-3
lines changed

5 files changed

+34
-3
lines changed

Source/Testably.Abstractions.Testing/TimeProvider.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public static ITimeProvider Random()
3434
/// <summary>
3535
/// Initializes the <see cref="MockTimeSystem.TimeProvider" /> with the specified <paramref name="time" />.
3636
/// </summary>
37+
/// <remarks>
38+
/// If the <paramref name="time" /> has Kind DateTimeKind.Unspecified it will be treated as if it had Kind DateTimeKind.Utc.
39+
/// </remarks>
3740
public static ITimeProvider Use(DateTime time)
3841
{
3942
return new TimeProviderMock(time, "Fixed");

Source/Testably.Abstractions.Testing/TimeSystem/TimeProviderMock.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ internal sealed class TimeProviderMock : ITimeProvider
1717

1818
public TimeProviderMock(DateTime now, string description)
1919
{
20-
_now = now;
20+
_now = now.Kind == DateTimeKind.Unspecified
21+
? DateTime.SpecifyKind(now, DateTimeKind.Utc)
22+
: now;
23+
2124
_description = description;
2225
}
2326

Tests/Testably.Abstractions.Testing.Tests/MockTimeSystemTests.cs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ async Task Act()
5252
await That(Act).ThrowsExactly<ArgumentOutOfRangeException>().WithParamName("delay");
5353
}
5454

55+
[Theory]
56+
[InlineData(DateTimeKind.Local)]
57+
[InlineData(DateTimeKind.Unspecified)]
58+
[InlineData(DateTimeKind.Utc)]
59+
public async Task DifferenceBetweenDateTimeNowAndDateTimeUtcNow_ShouldBeLocalTimeZoneOffsetFromUtc(DateTimeKind dateTimeKind)
60+
{
61+
DateTime now = TimeTestHelper.GetRandomTime(DateTimeKind.Local);
62+
63+
var expectedDifference = TimeZoneInfo.Local.GetUtcOffset(now);
64+
65+
MockTimeSystem timeSystem = new(DateTime.SpecifyKind(now, dateTimeKind));
66+
var actualDifference = timeSystem.DateTime.Now - timeSystem.DateTime.UtcNow;
67+
68+
await That(actualDifference).IsEqualTo(expectedDifference);
69+
}
70+
5571
[Fact]
5672
public async Task Sleep_Infinite_ShouldNotThrowException()
5773
{
@@ -102,7 +118,7 @@ public async Task ToString_WithFixedContainer_ShouldContainTimeProvider()
102118
string result = timeSystem.ToString();
103119

104120
await That(result).Contains("Fixed");
105-
await That(result).Contains($"{now.ToUniversalTime()}Z");
121+
await That(result).Contains($"{now}Z");
106122
}
107123

108124
[Fact]

Tests/Testably.Abstractions.Testing.Tests/TimeProviderTests.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,13 @@ public async Task Use_ShouldReturnFixedDateTime()
5959
await That(result1).IsEqualTo(now);
6060
await That(result2).IsEqualTo(now);
6161
}
62+
63+
[Fact]
64+
public async Task Use_UnspecifiedKind_ShouldConvertToUtcDateTime()
65+
{
66+
DateTime unspecifiedTime = TimeTestHelper.GetRandomTime(DateTimeKind.Unspecified);
67+
ITimeProvider timeProvider = TimeProvider.Use(unspecifiedTime);
68+
DateTime result = timeProvider.Read();
69+
await That(result).IsEqualTo(DateTime.SpecifyKind(unspecifiedTime, DateTimeKind.Utc));
70+
}
6271
}

Tests/Testably.Abstractions.Testing.Tests/TimeSystem/NotificationHandlerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public async Task OnDateTimeRead_MultipleCallbacks_ShouldAllBeCalled()
6060
[Fact]
6161
public async Task OnDateTimeRead_Today_ShouldExecuteCallbackWithCorrectParameter()
6262
{
63-
DateTime expectedTime = TimeTestHelper.GetRandomTime().Date;
63+
DateTime expectedTime = TimeTestHelper.GetRandomTime(DateTimeKind.Local).Date;
6464
MockTimeSystem timeSystem = new(expectedTime);
6565
DateTime? receivedTime = null;
6666

0 commit comments

Comments
 (0)