Guides

Time and precision

One clock per test, frozen or shared with the code under test, rounded to match your database.

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 clockFrozen when Start is called. Every entity in the test sees the same Now.
a TimeProviderRead live. Advance a FakeTimeProvider during the test.
An expiry test is easier to reason about when setup and the code under test read the same clock. Pass one 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));

Frozen by default

Without a provider, Biome captures TimeProvider.System.GetUtcNow() once at Start. Entities built a moment apart still get identical timestamps:

CodeReviewTests.cs
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:

CommerceTests.cs
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:

Relative
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

A timestamp can be correct in memory and still change on a database round trip. Biome rounds clock readings to 1 µs by default to match Postgres 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.

PostgresTimeTests.cs
// 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))
OptionEffect
default1 µs (TimeSpan.FromTicks(10)).
options.TimePrecision(TimeSpan.FromMilliseconds(1))Round to a millisecond.
options.TimePrecision(TimeSpan.Zero)Turn rounding off.
If timestamps differ by ticks or microseconds after a read, check whether setup used a separate wall-clock reading. Use the session clock or the shared FakeTimeProvider for comparisons, so precision and elapsed time do not introduce an unrelated difference.