Time and precision
One clock
Reading wall-clock time separately for each row can give related entities slightly different timestamps. That difference can obscure what the test intended. Biome gives each session one clock, accessed in blueprints through ctx.Now, ctx.Ago(…), and ctx.FromNow(…).
| You pass… | ctx.Now is… |
|---|---|
| no clock | Frozen when Start is called. Every entity in the test sees the same Now. |
a TimeProvider | Read live. Advance a FakeTimeProvider during the test. |
FakeTimeProvider to both, then advance it to exercise the transition.var time = new FakeTimeProvider(
new DateTimeOffset(2026, 10, 11, 12, 0, 0, TimeSpan.Zero)
);
await TestBiome
.Start(out _, time: time)
.AddLockedReview(out var activeLock) // ExpiresAtUtc = 12:05
.BuildAsync();
// the SUT reads the same clock: the lock has expired
time.Advance(TimeSpan.FromMinutes(6));
// Wall-clock time in setup, a separate fake clock
// in the SUT, and arithmetic by hand.
var start = new DateTimeOffset(2026, 10, 11, 12, 0, 0, TimeSpan.Zero);
var time = new FakeTimeProvider(start);
var repo = new CodeRepository
{ /* … */
CreatedAtUtc = start,
};
var pr = new PullRequestRecord
{ /* … */
CreatedAtUtc = start,
};
var review = new CodeReviewRecord
{ /* … */
CreatedAtUtc = start,
};
var activeLock = new ActiveCodeReviewLock
{
/* … */
AcquiredAtUtc = start,
ExpiresAtUtc = start.AddMinutes(5), // repeated in every test that needs a lock
};
_db.AddRange(seed.Owner, seed.Organization, repo, pr, review, activeLock);
await _db.SaveChangesAsync();
time.Advance(TimeSpan.FromMinutes(6));
Frozen by default
Without a provider, Biome captures TimeProvider.System.GetUtcNow() once at Start. Entities built a moment apart still get identical timestamps:
var biome = TestBiome.Start(out _);
biome.AddCodeRepository(out var first);
await Task.Delay(20);
await biome.AddCodeRepository(out var second).BuildAsync();
// second.CreatedAt == first.CreatedAt, and first.CreatedAt.Ticks % 10 == 0
A live clock between Adds
Adds are eager, so a test can advance the provider between Add calls before the single save:
var instant = new DateTimeOffset(2026, 10, 11, 12, 0, 0, TimeSpan.Zero);
var time = new FakeTimeProvider(instant);
var biome = TestBiome.Start(out _, time: time);
biome.AddTrackedOrder(out var first);
// Eager construction lets a test advance time
// between Add calls before the single save.
time.Advance(TimeSpan.FromHours(2));
await biome.Under(first.Shipment).AddTrackingEvent(out var later).BuildAsync();
// first.OccurredAt is unchanged; later.OccurredAt uses the advanced clock.
Relative times
Use the two-parameter form with ctx.Ago / ctx.FromNow. It works with any clock:
await TestBiome
.Start(out _)
.AddCodeRepository(out _)
.AddPullRequest(out _)
.AddOne(
Blueprints.Review,
out var stale,
(r, ctx) => r.CreatedAtUtc = ctx.Ago(TimeSpan.FromHours(2))
)
.BuildAsync();
Precision rounding
timestamptz, so values built from those readings retain their precision when saved..NET stores 100 ns ticks. Postgres stores microseconds. An unrounded DateTimeOffset.UtcNow loses its last digit on a round trip, and an equality assertion then fails in CI only. Biome truncates every reading of "now" (frozen and test-supplied) before Ago and FromNow derive from it.
// Seven fractional digits: the last tick is below Postgres precision.
var instant = new DateTimeOffset(2026, 10, 11, 12, 0, 0, TimeSpan.Zero).AddTicks(
1_234_567
);
var time = new FakeTimeProvider(instant);
await TestBiome.Start(db, out _, time).AddCodeRepository(out var repo).BuildAsync();
var saved = await read.Repositories.SingleAsync(r => r.Id == repo.Id);
// saved.CreatedAt == repo.CreatedAt (and == instant.AddTicks(-7))
| Option | Effect |
|---|---|
| default | 1 µs (TimeSpan.FromTicks(10)). |
options.TimePrecision(TimeSpan.FromMilliseconds(1)) | Round to a millisecond. |
options.TimePrecision(TimeSpan.Zero) | Turn rounding off. |
FakeTimeProvider for comparisons, so precision and elapsed time do not introduce an unrelated difference.