A fixture establishes boundaries; seed data establishes repeatable initial facts. Do not make every test share one universal database. Pure domain rules use object builders, HTTP pipelines use WebApplicationFactory, real persistence and messaging use Testcontainers, and platform initialization uses module-owned ISeedStep instances.
Fixture selection matrix
| Fact to verify | Recommended fixture | Avoid |
|---|---|---|
| Aggregate transition | Direct object/test builder | Starting a host |
| Handler and pure policy | Explicit fake port and fixed clock | Mocking private methods |
| Middleware, auth, Problem Details | TestApiFactory / WebApplicationFactory | Calling only the handler |
| SQL translation, transaction, concurrency | Testcontainers SQL Server | EF InMemory |
| CAP/Outbox and broker | SQL Server + RabbitMQ containers | In-memory event list |
| ORM parity | One contract against SqlSugar and EF Core | Comparing SQL strings |
| Package consumption | Temp directory + isolated feed/cache | Product ProjectReference |
Test type determines cost and evidence. Enumerate branches at the cheapest layer and reserve high layers for critical cross-boundary paths.
Current integration-fixture shapes
Non-Docker API tests use TestApiFactory for Shell composition. GoldenUseCaseApiFactory starts SQL Server, RabbitMQ, and a real API Host. Store, repository, Website, CAP, audit, and seeder contracts commonly implement IAsyncLifetime to own containers directly.
tests/BitzOrcas.Integration.Tests/AssemblyInfo.cs disables parallel execution so containers do not compete for Docker resources. Do not re-enable class-level parallelism for superficial speed.
# No Docker: API Shell and cross-layer safe-degradation behavior.dotnet test tests/BitzOrcas.Integration.Tests \ --configuration Release --filter 'Category!=Docker'
# Docker: run only seeder contracts and avoid unrelated containers.dotnet test tests/BitzOrcas.Integration.Tests \ --configuration Release \ --filter 'Category=Docker&FullyQualifiedName~Seeders.'Seed-asset ownership
CSV assets live with the owner module’s Infrastructure assembly. Current assets span Identity, MasterData, Authorization, Menu, PlatformBilling, Catalog, Numbering, and a small Framework compatibility location. One centralized SqlSugar directory no longer represents the system.
# Physical assets; runtime execution still depends on host DI composition.find src/Platform src/Framework \ -path '*/Seeders/Assets/*.csv' -print | sort
# Inspect step identity, order, scope, and explicit dependencies.rg -n 'SeedId =>|Order =>|SeedScope =>|DependsOn =>' \ src/Platform src/Framework -g '*SeedStep.cs'Filename prefixes aid review and align with Order, but SeedRunner schedules registered ISeedStep.Order values. Duplicate SeedId or Order and missing DependsOn fail before execution.
ORM-neutral seed base
The standard base is EntitySetCsvSeedStepBase<TEntity>, not the old CsvSeedStepBase<T>. It reads embedded CSV from the step assembly, queries IEntitySet<TEntity> by stable business key, then inserts or copies owner-managed fields.
public sealed class TestCatalogSeedStep( IEntitySet<TestCatalogRecord> rows, CsvSeedReader reader, ILogger<TestCatalogSeedStep> logger) : EntitySetCsvSeedStepBase<TestCatalogRecord>(rows, reader, logger){ public override int Order => 960; public override string SeedId => "test_catalog"; public override SeedScope SeedScope => SeedScope.Demo; protected override string CsvFileName => "960-test_catalog.csv";
// Code survives replay; a generated database Id is not a stable match key. protected override string[] WhereColumns => [nameof(TestCatalogRecord.Code)]; protected override Expression<Func<TestCatalogRecord, bool>> Match(TestCatalogRecord source) => row => row.Code == source.Code;
protected override void Copy(TestCatalogRecord source, TestCatalogRecord target) { // Copy CSV-owned values while preserving identity, tenant, and runtime audit data. target.Name = source.Name; target.IsEnabled = source.IsEnabled; }}Order 960 is illustrative and must be checked for global uniqueness in the real target composition. An owner chooses and guards its range; copying the number does not create a convention.
Scope and environment
| Scope | Development/Demo | Staging/Production | Purpose |
|---|---|---|---|
Global | Run | Run | Platform-wide language, currency, and region data |
Tenant | Run | Run | Default-tenant organization and setting overrides |
ProductionSafe | Run | Run | Required platform definitions and master data |
Demo | Run | Skip automatically | Sample catalog, non-production accounts/content |
BitzOrcasSeedOptions can disable the runner, set SkipSeedIds, or configure a CSV override root. A test override belongs in a temporary directory and is deleted afterward. Production evidence must not depend on an uncommitted developer-machine CSV.
Deterministic data rules
- use test-owned TenantId, business keys, and timestamps; never require another test to run first;
- obtain time from
IAppClockor an explicit argument, not a racingUtcNowassertion; - record random seeds so failures replay;
- create and clean each test’s facts, or isolate with a transaction/container;
- assert business keys and observable results, not exact generated identity numbers;
- use synchronization barriers for concurrency, not
Task.Delaytiming guesses; - keep real passwords, tokens, licenses, customer data, and machine paths out of CSV.
Testcontainers lifecycle
public sealed class CatalogSqlContractTests : IAsyncLifetime{ private readonly MsSqlContainer database = new MsSqlBuilder().Build();
public async Task InitializeAsync() { // Container start is followed by a query-level readiness check. await database.StartAsync(); await MsSqlContainerReadiness.WaitForQueryAsync(database.GetConnectionString()); }
// xUnit invokes async disposal after assertion failures, reclaiming the container. public Task DisposeAsync() => database.DisposeAsync().AsTask();}On failure, preserve container logs, readiness stage, and the first business assertion. Do not merely extend a Testcontainers timeout; distinguish image pull, daemon, resources, readiness, and schema failures.
Idempotency and partial-failure tests
A seed step covers first run on empty schema, replay, owner-managed value updates, duplicate business-key rejection, missing dependency rejection, and Demo skipping in production. Parent/child steps also verify DependsOn and foreign-key order.
“Running twice does not throw” is insufficient. After replay, row count remains stable, owner values converge, runtime fields remain intact, and report status/exit code are correct.
Fixture review checklist
- fixture layer matches the fact being proved;
- Docker tests carry
Category=Dockerand enter the right CI shard; - no test depends on order, fixed ports, shared users, or historical databases;
- containers have readiness, cancellation, and deterministic cleanup;
- SeedId, Order, and DependsOn are valid in the target composition;
- Global/Tenant/Demo/ProductionSafe scope matches the data;
- replay, partial failure, and concurrency have assertions;
- logs and failure messages expose no connection strings or secrets.
Continue with the seed reference, integration tests, and running tests.