Constructor setup
A fresh baseline for each test
Several tests in a class often start with the same owner, organization and repository. Repeating that chain in every method obscures the part of the scenario that differs. Put the common starting graph in the instance constructor, then let each test add the rows and properties its behavior needs.
Both TUnit and xUnit create a fresh test class instance for each test. The constructor calls Start for that instance, so every test gets its own seed, session, entities and sequence state. The methods reuse the setup recipe; each execution builds a new graph.
Keep the baseline builder in an instance field. Leave the session open in the constructor, extend it in the test, and call BuildAsync after the test's full setup is ready.
Constructor and test continuations
These examples use the CodeReviewSeed, entities, traits and TestBiome entry point from the repository's CodeReviews sample. In your project, use your own seed type and graft methods. The field's anchor type is CodeRepository[], because every Add returns a builder anchored to its output array, even for a singular Add.
using TUnit.Assertions;
using TUnit.Core;
using Zeeq.Biome;
namespace Zeeq.Biome.Tests.CodeReviews;
public sealed class PullRequestTests
{
private readonly BiomeBuilder<CodeReviewSeed, CodeRepository[]> _base;
private readonly CodeRepository _repository;
public PullRequestTests()
{
// Runs for each test instance; build only the common parent here.
_base = TestBiome.Start(out _).AddCodeRepository(out _repository);
}
[Test]
public async Task Closed_pull_request()
{
await _base
.AddPullRequest(out var pr, PullRequests.Closed)
.BuildAsync(); // Complete this test's setup once.
await Assert.That(pr.Repository).IsSameReferenceAs(_repository);
await Assert.That(pr.Closed).IsTrue();
await Assert.That(pr.Number).IsEqualTo(1);
}
[Test]
public async Task Merged_pull_request()
{
// Another instance: its repository and sequence are fresh.
await _base
.AddPullRequest(out var pr, PullRequests.Closed + PullRequests.Merged)
.BuildAsync();
await Assert.That(pr.Repository).IsSameReferenceAs(_repository);
await Assert.That(pr.Merged).IsTrue();
await Assert.That(pr.Number).IsEqualTo(1);
}
}
using Xunit;
using Zeeq.Biome;
namespace Zeeq.Biome.Tests.CodeReviews;
public sealed class PullRequestTests
{
private readonly BiomeBuilder<CodeReviewSeed, CodeRepository[]> _base;
private readonly CodeRepository _repository;
public PullRequestTests()
{
// Runs for each test instance; build only the common parent here.
_base = TestBiome.Start(out _).AddCodeRepository(out _repository);
}
[Fact]
public async Task Closed_pull_request()
{
await _base
.AddPullRequest(out var pr, PullRequests.Closed)
.BuildAsync(); // Complete this test's setup once.
Assert.Same(_repository, pr.Repository);
Assert.True(pr.Closed);
Assert.Equal(1, pr.Number);
}
[Fact]
public async Task Merged_pull_request()
{
// Another instance: its repository and sequence are fresh.
await _base
.AddPullRequest(out var pr, PullRequests.Closed + PullRequests.Merged)
.BuildAsync();
Assert.Same(_repository, pr.Repository);
Assert.True(pr.Merged);
Assert.Equal(1, pr.Number);
}
}
The assertions expose the setup strategy: both tests start with pull request number 1, and each request belongs to the repository created by its own constructor. In a behavior test, call the code under test after the setup and assert its result.
These examples omit the store and use NullStore. BuildAsync is optional for that case; it is shown at the final setup boundary so the same pattern works when TestBiome.Start receives a store.
The field keeps its anchor
An Add returns a new builder view over the same session. It does not move the anchor of a builder you already kept. Here, _base stays anchored to the constructor's repository, so another _base.AddPullRequest(...) in the same test creates a sibling under that repository. Sequences continue within that test's session.
When a continuation needs an earlier parent, use .Under(_repository). See Parents and children for branching. Keeping a base builder does not clone or reset the graph: all branches in one test belong to the same session and are completed by one BuildAsync.
Construct synchronously, save after extending
Adds create entities immediately, so the constructor can prepare the baseline without asynchronous work. Constructors cannot await BuildAsync, and saving closes the session to further Adds. Keep the save in the test after its final Add; avoid blocking on asynchronous setup with .Result or .GetAwaiter().GetResult().
For EF Core, create a context for each test instance and pass it to TestBiome.Start. After the test extends the baseline, call BuildAsync once and clear the change tracker before the code under test reads. Dispose the context through the framework's per-test cleanup lifecycle. See EF Core store.
If creating the context or another dependency requires asynchronous initialization, use TUnit's per-test hooks or xUnit's IAsyncLifetime to prepare those resources. TUnit property injection happens after the constructor, so a constructor cannot use a property-injected context; start the baseline in a per-test hook once that dependency is available. Keep its Biome session open for the test's continuation.
Keep mutable state per test
Create the seed, builder, entities, mutable clock and context for each test instance. A static builder, a shared fixture containing a builder, or a shared fake clock would carry mutations between tests. Share immutable blueprints and frozen BiomeOptions; each constructor still calls Start to create a fresh session.
If the test advances time, create its provider with the baseline and pass the same instance to Biome and the code under test. See Time and precision.
Separate test instances isolate the in-memory graph. A shared database still needs test-specific rows and an isolation or cleanup strategy; a fresh constructor alone does not isolate database state.
Keep the constructor's baseline small: only rows every test in the class needs. Put scenario-specific values in the test's continuation, or use a named scenario helper when only some tests share a longer chain. That leaves the behavior under test visible at the call site.