FakeTimeProvider.Advance/SetUtcNow does not behave as expected
See original GitHub issueThe current implementation of FakeTimeProvider
s Advance(TimeSpan)
and SetUtcNow(DateTimeOffset)
does not behave as expected.
There are a few different scenarios, so let me take them one at a time:
Scenario 1: A timer with dueTime = 5 second
and period = 5 seconds
.
Calling Advance(TimeSpan.FromSeconds(5))
at time 0 should invoke the timer callback 1 time (works).
Calling Advance(TimeSpan.FromSeconds(10))
at time 0 should invoke the timer callback 2 time (does not work).
Here are two tests that cover this:
[Fact]
public void Timer_callback_invoked_multiple_times_single_advance()
{
var sut = new FakeTimeProvider();
var callbackCount = 0;
var dueTime = TimeSpan.FromSeconds(3);
var period = TimeSpan.FromSeconds(5);
using var timer = sut.CreateTimer(_ => callbackCount++, null, dueTime, period);
sut.Advance(TimeSpan.FromSeconds(13));
callbackCount.Should().Be(3);
}
[Fact]
public void GetUtcNow_matches_time_at_callback_time()
{
var sut = new FakeTimeProvider();
var startTime = sut.GetUtcNow();
var callbackTimes = new List<DateTimeOffset>();
var interval = TimeSpan.FromSeconds(3);
using var timer = sut.CreateTimer(_ => callbackTimes.Add(sut.GetUtcNow()), null, interval, interval);
sut.Advance(interval + interval + interval);
callbackTimes.Should().ContainInOrder(
startTime + interval,
startTime + interval + interval,
startTime + interval + interval + interval);
}
Scenario 2: A periodic timer with a period = 10 seconds
and a delay of 3 seconds inserted after each WaitForNextTickAsync
completes.
The point of this scenario is that during a timer callback, another thing can be scheduled.
Calling Advance(TimeSpan.FromSeconds(23))
at time 0 should result in callback invocations at times 10, 13, 20, and 23.
Here is a test that covers this:
[Fact]
public async Task Callbacks_happens_in_schedule_order()
{
var sut = new FakeTimeProvider();
var periodicTimer = sut.CreatePeriodicTimer(TimeSpan.FromSeconds(10));
var startTime = sut.GetUtcNow();
var callbacks = new List<DateTimeOffset>();
var callbacksTask = AsyncCallbacks(periodicTimer);
sut.Advance(TimeSpan.FromSeconds(23));
callbacks.Should().ContainInOrder(
startTime + TimeSpan.FromSeconds(10),
startTime + TimeSpan.FromSeconds(13),
startTime + TimeSpan.FromSeconds(20),
startTime + TimeSpan.FromSeconds(23));
periodicTimer.Dispose();
await callbacksTask;
async Task AsyncCallbacks(PeriodicTimer periodicTimer)
{
while (await periodicTimer.WaitForNextTickAsync().ConfigureAwait(false))
{
callbacks.Add(sut.GetUtcNow());
await sut.Delay(TimeSpan.FromSeconds(3));
callbacks.Add(sut.GetUtcNow());
}
}
}
I could see the benefit in a SkipAhead
/BendSpaceAndTime
or similar method, that allows the test user to skip forward in time from t0
to tN
and explicitly not invoke any callbacks between t0
and tN
, and only invoke callbacks set to trigger at tN
. However, while using my own version of FakeTimeProvider in production for the last six months I have yet to have a need. I have however needed both of the scenarios described above in my testing.
Ps. If you want to compare notes, here is my implementation, ManualTimeProvider
.
Issue Analytics
- State:
- Created 4 months ago
- Comments:21 (21 by maintainers)
Top GitHub Comments
The motivation for TimeProvider/FakeTimeProvider came from needing to write tests for all the code in this repo. Then we started to look around and found literally hundreds of variations of IClock types within Microsoft services. It became very clear that this needed a universal abstraction.
That’s actually how I thought it should have worked initially, but AFAIR the existing implementation and tests were not behaving this way. I’ll talk with internal stakeholders and comment back here.