Seeds and TestBiome
The seed
Many scenarios share a small starting point: an owner, an organization, or a tenant ID. A seed (TSeed) carries that baseline into the blueprints so each test does not have to pass it around. Biome never inspects its shape, so it can be a record, a string, or an existing fixture object.
public sealed record ZeeqSeed(User Owner, Organization Organization)
{
public static ZeeqSeed Generate()
{
var owner = new User
{
Id = $"user_{Guid.CreateVersion7():N}",
DisplayName = "Owner",
};
var organization = new Organization
{
Id = $"org_{Guid.CreateVersion7():N}",
DisplayName = "Test Organization",
CreatedByUserId = owner.Id,
};
return new(owner, organization);
}
}
The seed is created outside the session for each test, so a new test never shares rows with another.
public sealed record TenantSeed(string Actor, int Tenant);.Seed rows
A seed can contain configuration as well as entities, so Biome cannot assume which objects belong in the database. Select those rows with SeedEntities. They are saved before the created entities, in the order you list them.
private static readonly BiomeOptions<ZeeqSeed> Options =
new BiomeOptions<ZeeqSeed>().SeedEntities(seed =>
[seed.Owner, seed.Organization]
); // foreign-key-safe order
Seed rows also count as owned by the session, so a test can anchor children to one with .Under(seed.Owner). A null entry in the list throws immediately.
TestBiome: one call to start
Most tests use the same seed factory and options. Wrapping Biome.Start once keeps that wiring in one place and gives each test a short entry point.
public static partial class TestBiome
{
private static readonly BiomeOptions<ZeeqSeed> Options =
new BiomeOptions<ZeeqSeed>().SeedEntities(seed =>
[seed.Owner, seed.Organization]
);
// No database: NullStore, entities still exist after each Add.
public static BiomeBuilder<ZeeqSeed, ZeeqSeed> Start(
out ZeeqSeed seed,
IBiomeStore? store = null,
TimeProvider? time = null
)
{
seed = ZeeqSeed.Generate();
return Biome.Start(seed, Options, store, time);
}
// With EF Core: same session, saved through a DbContext.
public static BiomeBuilder<ZeeqSeed, ZeeqSeed> Start(
DbContext db,
out ZeeqSeed seed,
TimeProvider? time = null
) => Start(out seed, db.AsBiomeStore(), time);
}
// Every test class repeats this fixture code, or inherits it from a base class.
public abstract class DatabaseTestBase
{
protected AppDb Db = null!;
protected User Owner = null!;
protected Organization Organization = null!;
[Before(Test)]
public async Task SeedAsync()
{
Owner = new User
{
Id = $"user_{Guid.CreateVersion7():N}",
DisplayName = "Owner",
};
Organization = new Organization
{
Id = $"org_{Guid.CreateVersion7():N}",
DisplayName = "Test Organization",
CreatedByUserId = Owner.Id,
};
Db.AddRange(Owner, Organization);
// every test pays for the baseline, needed or not
await Db.SaveChangesAsync();
}
}
Biome.Start(seed, options, store, time) returns a BiomeBuilder<TSeed, TSeed>. A null store means NullStore. A null time means a frozen clock.
When several methods need the same starting graph, call TestBiome.Start in the test class's instance constructor and keep the baseline builder in an instance field. Each test instance gets a fresh session and extends it for its own scenario. See Constructor setup for TUnit and xUnit examples.
Options are shared and frozen
Sharing configuration is useful until one test changes it for another. Biome freezes options on first use and keeps each scenario's mutable state in its own session:
- Options are configured once, as a
static readonlyfield. - They freeze on the first
Start. Every later change (SeedEntities,Default,BeforeSave,AfterSave,TimePrecision,Ids) throwsInvalidOperationException. - Each
Startcreates a new session, so each test gets its own entities, sequences and clock. Parallel tests are safe.
TestBiome a factory and build a fresh instance, as the library's own CustomSeed tests do:internal static class TestBiome
{
internal static BiomeOptions<CustomSeed> NewOptions() =>
new BiomeOptions<CustomSeed>().Default<IHasCreatedBy, string?>(
n => n.CreatedBy,
(_, ctx) => ctx.Seed.Actor
);
private static readonly BiomeOptions<CustomSeed> Options = NewOptions();
internal static BiomeBuilder<CustomSeed, CustomSeed> Start(
out CustomSeed seed,
IBiomeStore? store = null,
BiomeOptions<CustomSeed>? options = null
)
{
seed = new("actor", 42);
return Biome.Start(seed, options ?? Options, store);
}
}
A test then builds its own options, for example TestBiome.NewOptions().BeforeSave(...), and passes them in. See Custom seed shapes.
A seed with timestamps
ctx.Now. If seed timestamps are compared with database values, round them yourself, or set them from the same TimeProvider you pass to Start.An existing fixture can be the seed directly:
new BiomeOptions<AppFixtureData>().SeedEntities(seed => seed.GetSeedEntities());