Skip to content

Commit 6f87f42

Browse files
committed
docs: clean up documentation, explain difference with FakeTimeProvider
1 parent 1b7fc65 commit 6f87f42

File tree

4 files changed

+246
-19
lines changed

4 files changed

+246
-19
lines changed

README.md

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,37 @@
11
# TimeProvider Extensions
22

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.
44

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.
66

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)`.
168

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`
1910

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/).
2312

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+
![Advancing time by three seconds in one second increments.](docs/advance-1-second.svg)
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+
![Advancing time by three seconds in one step using ManualTimeProvider.](docs/ManualTimeProvider-advance-3-seconds.svg)
24+
25+
However, `FakeTimeProvider` will invoke the timer callback at time `00:03` three times, as illustrated in the drawings below:
26+
27+
![Advancing time by three seconds in one step using FakeTimeProvider.](docs/FakeTimeProvider-advance-3-seconds.svg)
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:
2534

26-
- When using the `ManualTimeProvider` during testing to forward time, be aware of this issue: https://github.com/dotnet/runtime/issues/85326.
2735
- 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.
2836
- 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.
2937

@@ -138,7 +146,7 @@ public void DoStuff_does_stuff_every_11_seconds()
138146

139147
// Act
140148
_ = sut.DoStuff(CancellationToken.None);
141-
timeProvider.ForwardTime(TimeSpan.FromSeconds(11));
149+
timeProvider.Advance(TimeSpan.FromSeconds(11));
142150

143151
// Assert
144152
container
Lines changed: 17 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)