|
1 | 1 | # TimeProvider Extensions
|
2 | 2 |
|
3 |
| -Extensions for `System.TimeProvider` API. It includes a test version of the `TimeProvider` type, named `ManualTimeProvider`, that allows you to control the progress of time during testing deterministically. |
| 3 | +Extensions for [`System.TimeProvider`](https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider) API. It includes a version of the `TimeProvider` type, named `ManualTimeProvider`, that allows you to control the progress of time during testing deterministically. |
4 | 4 |
|
5 |
| -Currently, the following .NET time-based APIs are supported: |
| 5 | +An instance of `TimeProvider` for production use is available on the `TimeProvider.System` property, and `ManualTimeProvider` can be used during testing. |
6 | 6 |
|
7 |
| -| TimeProvider method | .NET API it replaces | |
8 |
| -|----------------------|----------------------| |
9 |
| -| `GetUtcNow()` method | `DateTimeOffset.UtcNow` property | |
10 |
| -| `CreateTimer()` method | `System.Threading.Timer` type | |
11 |
| -| `Delay(TimeSpan, CancellationToken)` method | `Task.Delay(TimeSpan, CancellationToken)` method | |
12 |
| -| `Task.WaitAsync(TimeSpan, TimeProvider)` method | `Task.WaitAsync(TimeSpan)` method | |
13 |
| -| `Task.WaitAsync(TimeSpan, TimeProvider, CancellationToken)` method | `Task.WaitAsync(TimeSpan, CancellationToken)` method | |
14 |
| -| `TimeProvider.CreatePeriodicTimer(TimeSpan)` method | `System.Threading.PeriodicTimer` type | |
15 |
| -| `TimeProvider.CreateCancellationTokenSource(TimeSpan)` method | `new CancellationTokenSource(TimeSpan)` method | |
| 7 | +During testing, you can move time forward by calling `Advance(TimeSpan)` or `SetUtcNow(DateTimeOffset)` on `ManualTimeProvider`. This allows you to write tests that run fast and predictable, even if the system under test pauses execution for multiple minutes using e.g. `TimeProvider.Delay(TimeSpan)`, the replacement for `Task.Delay(TimeSpan)`. |
16 | 8 |
|
17 |
| -The implementation of `TimeProvider` is abstract. An instance of `TimeProvider` for production use is available on the `TimeProvider.System` property, |
18 |
| -and `ManualTimeProvider` can be used during testing. |
| 9 | +## Difference between `ManualTimeProvider` and `FakeTimeProvider` |
19 | 10 |
|
20 |
| -During testing, you can move time forward by calling `ForwardTime(TimeSpan)` or `SetUtcNow(DateTimeOffset)` on `ManualTimeProvider`. This allows |
21 |
| -you to write tests that run fast and predictable, even if the system under test pauses execution for |
22 |
| -multiple minutes using e.g. `TimeProvider.Delay(TimeSpan)`, the replacement for `Task.Delay(TimeSpan)`. |
| 11 | +The .NET team has published a similar test specific time provider, the [`Microsoft.Extensions.Time.Testing.FakeTimeProvider`](https://www.nuget.org/packages/Microsoft.Extensions.Time.Testing.FakeTimeProvider/). |
23 | 12 |
|
24 |
| -## Known issues and limitations: |
| 13 | +The public API of both `FakeTimeProvider` and `ManualTimeProvider` are identical, but there are some differences in when time is set before timer callbacks. Lets illustrate this with an example: |
| 14 | + |
| 15 | +For example, if we create a `ITimer` with a *due time* and *period* set to **1 second**, the `DateTimeOffset` returned from `GetUtcNow()` during the timer callback may be different depending on the amount passed to `Advance()` (or `SetUtcNow()`). |
| 16 | + |
| 17 | +If we call `Advance(TimeSpan.FromSeconds(1))` three times, effectively moving time forward by three seconds, the timer callback will be invoked once at time 00:01`, `00:02`, and `00:03`, as illustarted in the drawing below. Both `FakeTimeProvider` and `ManualTimeProvider` behaves like this: |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +However, if we instead call `Advance(TimeSpan.FromSeconds(3))` once, the two implementations behave differently. `ManualTimeProvider` will invoke the timer callback at the same time as if we had called `Advance(TimeSpan.FromSeconds(1))` three times, as illustrated in the drawing below: |
| 22 | + |
| 23 | + |
| 24 | + |
| 25 | +However, `FakeTimeProvider` will invoke the timer callback at time `00:03` three times, as illustrated in the drawings below: |
| 26 | + |
| 27 | + |
| 28 | + |
| 29 | +Technically, both implementations are correct. The `ITimer` abstractions only promises to invoke the callback timer *on or after the duetime/period has elapsed*, never before. |
| 30 | + |
| 31 | +However, I strongly prefer the `ManualTimeProvider` approach since it behaves consistently independent of how time is moved forward. It seeems much more in the spirit of how a deterministic time provider should behave and avoids users being surprised when writing tests. I imaging users may get stuck for a while trying to debug why time is not set as expected due the suttle difference in behavior of `FakeTimeProvider`. |
| 32 | + |
| 33 | +## Known limitations: |
25 | 34 |
|
26 |
| -- When using the `ManualTimeProvider` during testing to forward time, be aware of this issue: https://github.com/dotnet/runtime/issues/85326. |
27 | 35 | - If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking `CancellationTokenSource.CancelAfter(TimeSpan)` on the `CancellationTokenSource` object returned by `CreateCancellationTokenSource(TimeSpan delay)`. This action will not terminate the initial timer indicated by the `delay` argument initially passed the `CreateCancellationTokenSource` method. However, this restriction does not apply on .NET 8.0 and later versions.
|
28 | 36 | - To enable controlling `PeriodicTimer` via `TimeProvider` in versions of .NET earlier than .NET 8.0, the `TimeProvider.CreatePeriodicTimer` returns a `PeriodicTimerWrapper` object instead of a `PeriodicTimer` object. The `PeriodicTimerWrapper` type is just a lightweight wrapper around the original `System.Threading.PeriodicTimer` and will behave identically to it.
|
29 | 37 |
|
@@ -138,7 +146,7 @@ public void DoStuff_does_stuff_every_11_seconds()
|
138 | 146 |
|
139 | 147 | // Act
|
140 | 148 | _ = sut.DoStuff(CancellationToken.None);
|
141 |
| - timeProvider.ForwardTime(TimeSpan.FromSeconds(11)); |
| 149 | + timeProvider.Advance(TimeSpan.FromSeconds(11)); |
142 | 150 |
|
143 | 151 | // Assert
|
144 | 152 | container
|
|
0 commit comments