TimeAssertions.TUnit
0.5.0
Prefix Reserved
dotnet add package TimeAssertions.TUnit --version 0.5.0
NuGet\Install-Package TimeAssertions.TUnit -Version 0.5.0
<PackageReference Include="TimeAssertions.TUnit" Version="0.5.0" />
<PackageVersion Include="TimeAssertions.TUnit" Version="0.5.0" />
<PackageReference Include="TimeAssertions.TUnit" />
paket add TimeAssertions.TUnit --version 0.5.0
#r "nuget: TimeAssertions.TUnit, 0.5.0"
#:package TimeAssertions.TUnit@0.5.0
#addin nuget:?package=TimeAssertions.TUnit&version=0.5.0
#tool nuget:?package=TimeAssertions.TUnit&version=0.5.0
TimeAssertions.TUnit
Scope: Test projects only. Not intended for production code.
TUnit-native fluent time-assertion DSL on top of Microsoft.Extensions.Time.Testing.FakeTimeProvider. Adds FakeTimeProvider state assertions, TimeProvider-aware DateTimeOffset recency / past / future checks, plus the cross-cutting .WithinTimeBudget(TimeSpan) chain extension. AOT-compatible, trimmable, no reflection.
Full documentation, "Why TimeProvider in tests", cookbook, design notes, and roadmap: github.com/JohnVerheij/TimeAssertions.TUnit
Install
dotnet add package TimeAssertions.TUnit
TimeAssertions (the framework-agnostic core) and Microsoft.Extensions.TimeProvider.Testing come transitively. Requirements: TUnit 1.45.8 or later, .NET 10.
The source-generated entry points (HasAdvancedExactly, HasAdvancedApproximately, HasUtcNow, HasUtcNowApproximately, IsRecent, IsBeforeNow, IsAfterNow, WithinTimeBudget, WithinTimeBudgetCapturing, WasInvokedAtMostOncePer) auto-import via TUnit.Assertions.Extensions. Add the following to a GlobalUsings.cs in your test project for the call-site and FakeTimeProvider namespaces:
global using Microsoft.Extensions.Time.Testing;
global using TimeAssertions;
global using TimeAssertions.TUnit;
Quick start
[Test]
public async Task PreReleaseExpiration_advances_state_after_clock_moves_forward()
{
var fakeTime = new FakeTimeProvider();
var service = new ExpirationService(fakeTime);
fakeTime.Advance(TimeSpan.FromMinutes(31));
await Assert.That(fakeTime).HasAdvancedExactly(TimeSpan.FromMinutes(31));
await Assert.That(service.LastRefresh).IsRecent(TimeSpan.FromSeconds(1), fakeTime);
// Cross-cutting timing budget on any behavioural assertion chain
await Assert.That(service.IsExpiredAsync())
.IsTrue()
.And.WithinTimeBudget(TimeSpan.FromMilliseconds(500));
}
Entry points
| Method | Purpose |
|---|---|
HasAdvancedExactly(TimeSpan) / HasAdvancedApproximately(total, tolerance) |
FakeTimeProvider advanced by exact / approximate amount (renamed from HasAdvanced / HasAdvancedBy in v0.2.0; old names [Obsolete] until v0.4.0) |
HasUtcNow(DateTimeOffset) / HasUtcNowApproximately(expected, tolerance) |
FakeTimeProvider is at exact / approximate moment |
IsRecent(TimeSpan, TimeProvider?) |
DateTimeOffset is within window before "now" of supplied (or system) clock |
IsBeforeNow(TimeProvider) / IsAfterNow(TimeProvider) |
DateTimeOffset ordering relative to supplied clock |
WithinTimeBudget(TimeSpan) |
Cross-cutting timing budget; chains via .And after any behavioural assertion |
WithinTimeBudgetCapturing(TimeSpan, Action<TimeSpan>) |
Same as WithinTimeBudget plus a callback that receives the measured elapsed on every evaluation path except external cancellation (added in v0.2.0; cancellation-skip behaviour added in v0.5.0) |
WasInvokedAtMostOncePer(this IReadOnlyList<DateTimeOffset>, TimeSpan interval) |
Rate-limit assertion on a recorded invocation log: every consecutive gap is at least interval (added in v0.5.0) |
Failure diagnostics
On a failed assertion, the exception message includes the elapsed / expected duration, the absolute drift, and (for budget overruns) the overshoot plus a grep-friendly (elapsed=Xms, budget=Yms, overrun=Zms) suffix for log scrapers. No Console.WriteLine debugging needed: every dimension you can assert on is also rendered in the failure message.
Full failure-diagnostics example, design notes, stability intent, and roadmap on GitHub.
Family
Part of an assertion family for TUnit:
- LogAssertions.TUnit
- SnapshotAssertions.TUnit
- MathAssertions.TUnit
- JsonAssertions.TUnit
- SseAssertions.TUnit
License
MIT. Copyright (c) 2026 John Verheij.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.TimeProvider.Testing (>= 10.6.0)
- TimeAssertions (>= 0.5.0)
- TUnit.Assertions (>= 1.45.8)
- TUnit.Core (>= 1.45.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
View the rendered release notes: https://github.com/JohnVerheij/TimeAssertions.TUnit/releases/tag/v0.5.0
Minor release that adds the first rate-limit assertion (`WasInvokedAtMostOncePer`) to the package, fixes a latent cancellation-handling behaviour in `WithinTimeBudget` / `WithinTimeBudgetCapturing` (external `OperationCanceledException` was wrapped as an assertion failure rather than propagated), and bumps the TUnit dependency to 1.45.8.
### Added
- **`RateLimitAssertions.WasInvokedAtMostOncePer(this IReadOnlyList<DateTimeOffset>, TimeSpan)`** asserts that consecutive timestamps in a recorded invocation log maintain at least the specified minimum interval. The first violating pair fails the assertion with a message naming the violating index, the observed gap, and the required minimum. Empty and single-element sequences pass trivially; the boundary case `gap == interval` passes. Source-generated via `[GenerateAssertion]` so the chain surface is `Assert.That(timestamps).WasInvokedAtMostOncePer(TimeSpan.FromSeconds(30))`.
- **`TimeAssertions.TimeRenderingHelpers.FormatRateLimitViolation(IReadOnlyList<DateTimeOffset>, int, TimeSpan, TimeSpan)`** renders the multi-line failure message for `WasInvokedAtMostOncePer` violations, with a grep-friendly fixed-unit parenthetical `(gap=Xms, minimum=Yms)` analogous to `FormatBudgetOverrun`'s `(elapsed=, budget=, overrun=)` trailer.
### Changed
- **BREAKING:** **`WithinTimeBudgetAssertion<T>` and `WithinTimeBudgetCapturingAssertion<T>`** now propagate external `OperationCanceledException` instead of wrapping it as an assertion failure. When a parent `[Timeout]` fires or the test runner cancels, the wrapped operation's `OperationCanceledException` flows through the assertion via `ExceptionDispatchInfo.Capture(...).Throw()` so the test is recorded as cancelled, not failed. The capturing variant additionally skips invoking the capture callback on cancellation: a partial elapsed from a cancelled operation would mislead consumers about the operation's real cost. Non-`OperationCanceledException` source exceptions continue to surface as `AssertionResult.Failed` exactly as before. Consumer tests that asserted `Throws<AssertionException>` against a cancelled `WithinTimeBudget` chain must update to expect `Throws<OperationCanceledException>` (or rely on the test runner's native cancellation reporting).
- **TUnit dependency bumped `1.44.0` -> `1.45.8`** (and the external-consumer smoke-test pin). The 1.45 line adds `CancellationToken` overloads to upstream `Eventually` / `WaitsFor`; the cookbook section "Waiting for an asynchronous effect after `Advance(...)`" documents the CT-bearing variant. The packed `README.md` requirement line bumps to "TUnit 1.45.8 or later" accordingly.
- `README.md`: added a fourth Entry-points subgroup "Rate-limit assertions on invocation timestamps" (with matching TOC anchor), plus updates to Package layout, Namespaces, and Install/Requirements that reference `WasInvokedAtMostOncePer()`.
- `README.md`: added the cookbook section "Verifying a periodic-probe suppression window", pairing recorded log timestamps with `WasInvokedAtMostOncePer` for the ping-escalation pattern.
- `README.md`: reframed the "Deferred items" entry for `HasActiveTimers` as "tracked upstream", explicit about the no-reflection rule and the consumer-side `ObservableTimeProvider` bridge workaround.
- `README.md`: expanded the family roster to six packages, adding `JsonAssertions.TUnit` and `SseAssertions.TUnit` to the "Family compatibility" section, the "Pair with" section, and the "shared across" line in Contributing.
- `SECURITY.md`: updated the supported-versions table to reflect 0.5.x as the current line and 0.4.x as the previous-stable line.
- `SECURITY.md`: updated the supply-chain attestation table to name the actual action versions in use today (`actions/attest-build-provenance@v4.1.0` and `actions/attest@v4.1.0`).