Skip to content

Commit

Permalink
docs: clean up documentation, explain difference with FakeTimeProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
egil committed Aug 21, 2023
1 parent 1b7fc65 commit 6f87f42
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 19 deletions.
46 changes: 27 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
# TimeProvider Extensions

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

Currently, the following .NET time-based APIs are supported:
An instance of `TimeProvider` for production use is available on the `TimeProvider.System` property, and `ManualTimeProvider` can be used during testing.

| TimeProvider method | .NET API it replaces |
|----------------------|----------------------|
| `GetUtcNow()` method | `DateTimeOffset.UtcNow` property |
| `CreateTimer()` method | `System.Threading.Timer` type |
| `Delay(TimeSpan, CancellationToken)` method | `Task.Delay(TimeSpan, CancellationToken)` method |
| `Task.WaitAsync(TimeSpan, TimeProvider)` method | `Task.WaitAsync(TimeSpan)` method |
| `Task.WaitAsync(TimeSpan, TimeProvider, CancellationToken)` method | `Task.WaitAsync(TimeSpan, CancellationToken)` method |
| `TimeProvider.CreatePeriodicTimer(TimeSpan)` method | `System.Threading.PeriodicTimer` type |
| `TimeProvider.CreateCancellationTokenSource(TimeSpan)` method | `new CancellationTokenSource(TimeSpan)` method |
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)`.

The implementation of `TimeProvider` is abstract. An instance of `TimeProvider` for production use is available on the `TimeProvider.System` property,
and `ManualTimeProvider` can be used during testing.
## Difference between `ManualTimeProvider` and `FakeTimeProvider`

During testing, you can move time forward by calling `ForwardTime(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)`.
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/).

## Known issues and limitations:
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:

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

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:

![Advancing time by three seconds in one second increments.](docs/advance-1-second.svg)

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:

![Advancing time by three seconds in one step using ManualTimeProvider.](docs/ManualTimeProvider-advance-3-seconds.svg)

However, `FakeTimeProvider` will invoke the timer callback at time `00:03` three times, as illustrated in the drawings below:

![Advancing time by three seconds in one step using FakeTimeProvider.](docs/FakeTimeProvider-advance-3-seconds.svg)

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.

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

## Known limitations:

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

Expand Down Expand Up @@ -138,7 +146,7 @@ public void DoStuff_does_stuff_every_11_seconds()

// Act
_ = sut.DoStuff(CancellationToken.None);
timeProvider.ForwardTime(TimeSpan.FromSeconds(11));
timeProvider.Advance(TimeSpan.FromSeconds(11));

// Assert
container
Expand Down
17 changes: 17 additions & 0 deletions docs/FakeTimeProvider-advance-3-seconds.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 6f87f42

Please sign in to comment.