Skip to content

Introduce a test isolation strategy based on event tags#4334

Open
theoema wants to merge 2 commits intoAxonIQ:mainfrom
theoema:feature/test-isolation-event-tags
Open

Introduce a test isolation strategy based on event tags#4334
theoema wants to merge 2 commits intoAxonIQ:mainfrom
theoema:feature/test-isolation-event-tags

Conversation

@theoema
Copy link
Copy Markdown
Contributor

@theoema theoema commented Mar 19, 2026

Summary

This implements the test isolation strategy I proposed in #4252. The idea is simple: each AxonTestFixture gets a unique UUID stamped on every message as metadata, persisted as a tag on stored events, and used to filter sourcing reads and recording assertions. This means multiple fixtures can share the same configuration and event store without cross-test interference — no purging, no cleanup, and safe for parallel execution.

It's fully opt-in. You register the TestIsolationEnhancer on your configurer and everything else is handled transparently:

var configurer = aC()
        .componentRegistry(cr -> cr.registerEnhancer(new TestIsolationEnhancer()));

var fixture = AxonTestFixture.with(configurer);
fixture.given().events(new StudentEnrolled("1", "Alice"))
       .when().command(new ChangeStudentName("1", "Bob"))
       .then().events(new StudentNameChanged("1", "Bob"));

Each fixture is isolated — events from one test are invisible to another, even though they share the same event store.

How it works

The TestIsolationEnhancer registers four pieces of infrastructure:

  • A CorrelationDataProvider that propagates the testId through the message chain
  • A TagResolver decorator that converts testId metadata into a Tag on stored events
  • An IsolatingEventStore decorator that adjusts sourcing criteria to include the testId tag — so the storage engine only returns matching events at query time
  • A FixtureCustomizer that wraps the command bus, event sink, and recording components with per-fixture stamping and filtering

For the recording side (assertions), each fixture gets IsolatingRecordingCommandBus and IsolatingRecordingEventSink wrappers that filter the shared recording lists by testId. This keeps the recording infrastructure (which is shared at the configuration level) unaware of isolation — the filtering is layered on top.

While building this, I also ran into an issue where and() was calling new AxonTestFixture(configuration, customization), which re-invoked the FixtureCustomizer and generated a new testId — making events from phase 1 invisible to phase 2. To fix this I introduced a TestContext that gets created once per fixture and reused across the entire phase chain. This also simplified the phase constructors from 4-7 individual parameters down to a single parameter, which is a nice cleanup on its own.

Scope

This addresses event sourcing isolation only. It doesn't solve projection/read-model isolation (where tests share a database for read models) — that's a different problem that would require user-side solutions since the framework doesn't own the projection storage.

Changes

  • TestIsolationEnhancer — the opt-in enhancer
  • IsolatingCommandBus, IsolatingEventSink — stamp outgoing messages with testId
  • IsolatingEventStore, IsolatingEventStoreTransaction — adjust sourcing criteria to include testId tag
  • IsolatingRecordingCommandBus, IsolatingRecordingEventSink — filter recorded messages by testId
  • FixtureCustomizer, FixtureConfiguration — per-fixture extension point and its input/output record
  • TestContext — per-test context that flows through the phase chain
  • AxonTestFixture and all phase classes refactored to use TestContext
  • Unit tests for all isolation components + integration test

Related to #4252

@theoema theoema requested a review from a team as a code owner March 19, 2026 21:29
@theoema theoema requested review from corradom, hatzlj and hjohn and removed request for a team March 19, 2026 21:29
@theoema
Copy link
Copy Markdown
Contributor Author

theoema commented Mar 19, 2026

I'm sure you're all very very busy prepping for the 5.1.0 release, looks like its coming out any time now, so seriously don't worry about reviewing this unless you have the time and want to!

@hatzlj hatzlj added the Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. label Mar 20, 2026
@hatzlj hatzlj added this to the Release 5.2.0 milestone Mar 20, 2026
@hatzlj
Copy link
Copy Markdown
Contributor

hatzlj commented Mar 20, 2026

Hey @theoema, this looks like a great addition - i've added it to the 5.2.0 milestone and we'll be sure to have a more detailed look once 5.1.0 is out.

I've seen there are test failures in the SpringBoot Starter Test module you could check out meanwhile if you don't mind.

@smcvb smcvb added the Type: Feature Use to signal an issue is completely new to the project. label Mar 20, 2026
theoema added 2 commits March 20, 2026 16:44
- Add TestIsolationEnhancer that enables test isolation by stamping messages
  with a unique testId, persisting it as event tags, and filtering sourcing
  reads and recording assertions
- Add IsolatingCommandBus and IsolatingEventSink that stamp outgoing messages
  with testId metadata
- Add IsolatingEventStore and IsolatingEventStoreTransaction that adjust
  sourcing criteria to include the testId tag
- Add IsolatingRecordingCommandBus and IsolatingRecordingEventSink that filter
  recorded messages by testId for assertions
- Add FixtureCustomizer interface as a per-fixture extension point invoked
  once per TestContext.create()
- Add FixtureConfiguration record as the narrow input/output type for
  FixtureCustomizer (commandBus, eventSink, recordings)
- Add TestContext as a per-test context object that holds resolved fixture
  components and flows through the phase chain (Given → When → Then → and()),
  ensuring stable test identity across chained phases
- Fix and() re-invoking FixtureCustomizer and generating a new testId by
  reusing the same TestContext via a package-private AxonTestFixture constructor
- Replace 6 individual fields in AxonTestFixture with single TestContext field
- Simplify all phase constructors (AxonTestGiven, AxonTestWhen,
  AxonTestThenMessage, AxonTestThenCommand, AxonTestThenEvent,
  AxonTestThenNothing) from 4-7 parameters to a single TestContext parameter
- Add unit tests for all isolation components and an integration test
  validating two fixtures sharing a configuration see isolated events
- Update AxonTestFixtureStatefulCommandHandlerTest and
  TestIsolationIntegrationTest for upstream EventSourcingRepository
  constructor change (Snapshotter parameter)
…nstructor

- Fix AxonSpringBootTestAnnotationTest reflection to navigate through
  testContext field instead of the removed customization field
- Migrate EventSourcingRepository usage from deprecated CriteriaResolver
  constructor to new SimpleSourcingHandler constructor in
  AxonTestFixtureStatefulCommandHandlerTest and
  TestIsolationIntegrationTest
@theoema theoema force-pushed the feature/test-isolation-event-tags branch from 7fde761 to db1fdd1 Compare March 20, 2026 16:04
@theoema
Copy link
Copy Markdown
Contributor Author

theoema commented Mar 20, 2026

Hey @hatzlj, thanks for taking a peek, I rebased off main and adjusted some stuff to new APIs and fixed the broken tests, have a good weekend!

@liamthorell
Copy link
Copy Markdown

@theoema great addition, keep up the good work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority 2: Should High priority. Ideally, these issues are part of the release they’re assigned to. Type: Feature Use to signal an issue is completely new to the project.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants