Shared Testing Framework
Shared Testing Framework
Section titled “Shared Testing Framework”This page describes the shared provider-agnostic test framework used by the storage-provider integration suites.
Overview
Section titled “Overview”Each storage provider (AzureStorage, CosmosDb, MongoDB, Postgres, SqlServer) has its own
integration test project. Instead of duplicating the same behavioural tests for every provider, the
repository defines two shared contract suites that run against every provider that advertises the
relevant capability:
- Event-store contract suite — runs against providers with an event store: Azure Storage, MongoDB, Postgres, SQL Server. (Cosmos DB has no event stream.)
- Snapshot-store contract suite — runs against providers with a query snapshot store: Cosmos DB, MongoDB, Postgres, SQL Server. (Azure Storage has no query snapshot store.)
Provider-specific behaviour (batch limits, index creation, JSON operators, query-translation boundaries, telemetry, storage layout) lives in per-provider guard tests in each integration project.
Layout
Section titled “Layout”| Path | Purpose |
|---|---|
src/tests/SharedTestingFramework/Contracts/ |
The shared contract suites. These are not compiled into the SharedTestingFramework assembly; each provider integration test project links them into its own compilation (see below). |
src/tests/SharedTestingFramework/Fixtures/ |
Provider Testcontainers fixtures. |
src/tests/<Provider>.IntegrationTests/Events/ |
The per-provider event-store wiring + guard tests. |
src/tests/<Provider>.IntegrationTests/Snapshots/ |
The per-provider snapshot-store wiring + guard tests. |
src/tests/<Provider>.IntegrationTests/Guards/ |
Provider-specific event-store guard tests. |
How the shared suites are wired
Section titled “How the shared suites are wired”TUnit uses compile-time discovery, so the [Test] methods must be discoverable from the test
assembly. The shared suites achieve this with three TUnit features:
[GenerateGenericTest(typeof(PersistenceAggregate))]on a generic test class makes TUnit generate a concrete test class for the supplied aggregate type.[ClassDataSource<TFixture>(Shared = SharedType.PerTestSession)]injects the provider fixture (one container per test session).[InheritsTests]picks up the[Test]methods declared on the shared generic base class (EventStoreContractTestsBase<TAggregate>/SnapshotStoreContractTestsBase<TAggregate>).
The contract sources under Contracts/ are linked directly into each provider test project’s
compilation (via <Compile Include="..\SharedTestingFramework\Contracts\..."> links) rather than
consumed cross-assembly. This is deliberate: TUnit’s TestMetadataGenerator reports error diagnostic
TUNIT0999 at the inherited method’s source location when an internal generation error occurs, and
Roslyn’s SourceProductionContext.ReportDiagnostic rejects diagnostics whose location is not part of
the compilation being analyzed (surfacing as CS8785). With a cross-assembly base class the location
points into SharedTestingFramework’s sources, outside the provider compilation, so the warning is
unavoidable. Linking the sources keeps every inherited [Test] method inside the provider compilation
and eliminates the failure. SharedTestingFramework therefore no longer carries the base [Test]
methods at all.
Each provider test project therefore adds a small derived class such as:
[GenerateGenericTest(typeof(PersistenceAggregate))][ClassDataSource<SqlServerEventStoreFixture>(Shared = SharedType.PerTestSession)][InheritsTests]public sealed class EventStoreContractTests<TAggregate>(SqlServerEventStoreFixture fixture) : EventStoreContractTestsBase<TAggregate> where TAggregate : class, IAggregateTest, new(){ protected override IEventStoreCore<TAggregate> CreateEventStore() => fixture.CreateEventStore<TAggregate>();
protected override IEventStoreCore<TAggregate> CreateEventStore(IAggregateChangeFeedNotifier<TAggregate>? notifier) => fixture.CreateEventStore(aggregateChangeNotifier: notifier);
protected override Task MarkEventTypesAsUnknownAsync(...) => /* provider-specific event rewrite */;}The shared base classes only exercise the public contracts (IEventStoreCore<T> and
IQueryableEventStoreCore<T>), observable state (save results, rehydrated aggregates, change-feed
notifications, event ranges, query results) and shared aggregates (PersistenceAggregate,
ComplexTestType). Provider internals are deliberately out of scope for the shared suites.
Where each suite runs
Section titled “Where each suite runs”| Suite | AzureStorage | CosmosDb | MongoDB | Postgres | SqlServer |
|---|---|---|---|---|---|
| Event-store contract suite | ✓ | — | ✓ | ✓ | ✓ |
| Snapshot-store contract suite | — | ✓ | ✓ | ✓ | ✓ |
| Provider guard / feature tests | ✓ | ✓ | ✓ | ✓ | ✓ |
Adding a new provider
Section titled “Adding a new provider”- Create the integration test project (see
project-placement-defaults), referencingSharedTestingFrameworkandSamples, and link the contract sources that the provider needs (theEventStore*and/orSnapshotStore*files underContracts/) — see the<Compile Include>links in the existing provider test projects. - Add an
EventStoreContractTests(and/orSnapshotStoreContractTests) derived class wired to the provider fixture. - Implement the provider-specific seams (
MarkEventTypesAsUnknownAsyncfor the unknown-event test,SnapshotAsyncfor the snapshot suite). - Add guard tests for capabilities that are not part of the shared contract.
Adding a shared test
Section titled “Adding a shared test”- Add the
[Test]method to the relevant shared base class underContracts/. - Add any data sources to the matching
*ContractTestDatastatic class. - The test runs automatically for every provider whose integration project links the contract sources and derives from the relevant base class.
Environment notes
Section titled “Environment notes”- Integration suites require Docker and the provider images (Testcontainers).
- The Azure/Cosmos/Mongo snapshot fixtures rely on Azurite; the SQL fixtures on a SQL Server image.
- The CI pipeline runs only the unit-test tree filter; integration suites are exercised locally or via an opt-in run.