[#729] Add Query Testing Support To AxonTestFixture#3944
[#729] Add Query Testing Support To AxonTestFixture#3944theoema wants to merge 3 commits intoAxonIQ:mainfrom
Conversation
Extends the AxonTestFixture with the ability to test query handlers using the same fluent API as command and event testing. This enables developers to verify query handling behavior in their test scenarios. Key additions: - RecordingQueryBus: Decorator that records all dispatched queries and their responses - Query When phase: New query() method to dispatch queries during test execution - Query Then phase: Assertion methods for validating query results (expectResult, expectResultSatisfies, success) - MessagesRecordingConfigurationEnhancer: Extended to register the RecordingQueryBus alongside existing recording components The implementation follows the established pattern of RecordingCommandBus and RecordingEventSink, providing consistent testing experience across all message types.
…roach Updates the query testing feature to automatically wait for asynchronous event processors before executing queries. This ensures the read model has been updated with all published events from the given() phase. Implementation: - Default 200ms delay before query execution to allow async event processing - Configurable delay via query(payload, Duration) overload - Duration.ZERO option to skip waiting for synchronous processors - Added expectQueryResult() and expectQueryResultSatisfies() convenience methods Test coverage: - Basic query testing with async event processors - Wrapped result objects (e.g., Result<T> patterns) - Null result handling - Custom assertions on query results The delay-based approach provides a pragmatic solution that works reliably across different event processor types and test scenarios. Future improvements may add deterministic tracking using StreamingEventProcessor.processingStatus().isCaughtUp() once in-memory event store behavior is better understood.
…hanism This commit adds comprehensive query testing support to AxonTestFixture with a deterministic mechanism for waiting on asynchronous event processors before executing queries. ## Key Features ### Deterministic Auto-Await Mechanism The query testing now uses a token-based approach to ensure read models are up-to-date before queries execute: 1. When `.query()` is called, the fixture automatically waits for all streaming event processors to catch up with the event store's head position 2. Uses `EventStore.latestToken()` to get the head tracking token 3. Polls each processor's `processingStatus()` every 50ms 4. Checks if each processor's current token covers/reaches the head token 5. Only executes the query once all processors have caught up 6. Default timeout of 5 seconds (configurable via `.query(payload, timeout)`) 7. Use `Duration.ZERO` to skip waiting for synchronous processors This eliminates the need for arbitrary delays and makes tests deterministic regardless of system load or event processing speed. ### Query Testing API - `.query(payload)` - Execute query with automatic 5-second timeout - `.query(payload, timeout)` - Execute query with custom timeout - `.then().expectResult(expected)` - Assert exact query result - `.then().expectQueryResult(expected)` - Alias for result assertion - `.then().expectResultSatisfies(consumer)` - Custom result assertions - `.then().expectQueryResultSatisfies(consumer)` - Alias for custom assertions - `.then().success()` - Assert no exceptions were thrown - `.then().exception(type)` - Assert exception type (checks wrapped cause) - `.then().exceptionSatisfies(consumer)` - Custom exception assertions ### Exception Handling Query handler exceptions are wrapped in `QueryExecutionException` by the framework. Tests check the cause using `.hasCauseInstanceOf()` to verify the actual exception thrown by the query handler. ### Architecture Changes **EventProcessorUtils (new)** - Extracted event processor waiting logic into reusable utility class - Located in `org.axonframework.test.util` package - Public static method `waitForEventProcessorsToCatchUp(config, timeout)` - Can be reused by other test utilities in the future **AxonTestWhen** - Added `.query()` methods to When phase - Integrated auto-await mechanism before query execution - Added null-check for query responses to handle missing handlers - Passes `RecordingQueryBus` to Then phase **AxonTestThenQuery (new)** - Implements Then.Query phase with result and exception assertions - Accepts `RecordingQueryBus` for potential future query assertions - Extends `AxonTestThenMessage` for event/command assertion support - Uses `PayloadMatcher` for deep equality checking ### Sample Domain for Testing Created comprehensive sample domain in `test/fixture/sampledomain`: **Domain Model** - `StudentReadModel` - Read model record - `GetStudentById` - Query with nested `Result` wrapper - `StudentRepository` - Repository interface with `findByIdOrThrow()` - `InMemoryStudentRepository` - Thread-safe in-memory implementation - `StudentNotFoundException` - Custom exception for missing students **Event Handling** - `StudentProjection` - Annotated event handler with `@EventHandler` - Processes `StudentNameChangedEvent` to update read model **Query Handling** - `StudentQueryHandler` - Annotated query handler with `@QueryHandler` - Returns wrapped `GetStudentById.Result` - Throws `StudentNotFoundException` when student not found ### Test Coverage Six comprehensive tests demonstrate all features: 1. `givenEventsWhenQueryThenExpectResult_Success` - Events published → read model updated → query returns wrapped result 2. `givenEventsWhenQueryThenExpectResultSatisfies_Success` - Custom assertions on query result structure 3. `givenNoEventsWhenQueryThenExpectException_Success` - Query with no data throws expected exception (checks cause) 4. `givenNoEventsWhenQueryThenExpectExceptionWithMessage_Success` - Exception with specific message verification 5. `givenNoEventsWhenQueryThenExpectExceptionSatisfies_Success` - Custom exception assertions with message content checks 6. `givenEventsWhenQueryThenSuccess_Success` - Success assertion when no exceptions occur All tests use `PooledStreamingEventProcessor` with async event processing to verify the deterministic auto-await mechanism works correctly.
|
Hey @abuijze @smcvb @MateuszNaKodach @zambrovski! Does this resemble the direction you wanted to go for query testing? I was thinking of adding subscriptionQuery testing aswell but wanted to check in with you before proceeding. |
smcvb
left a comment
There was a problem hiding this comment.
I did a rough skim of what you've provided, @theoema. Rough, mainly, because the AF-team still wants (and needs) to discuss what the API is going to be.
Although there's nothing wrong with providing a new API to us in a PR, the test fixture flow is already quite different from what it was in AF2, AF3, and AF4 with the current setup.
Next, the issue you attached it too, called projection testing, is not strictly scoping in query handler validation. It suggests we have a means to validate "the activities" performed by an event handler (and if not, I need to update the description). Again, not wrong to add queries in the mix, as we want verification for those as well.
Long story short, we're not suited yet to approve or request changes on this PR. Once we've had more discussions on the angle to take for projection testing and query handler validation, we'll be sure to update this PR too.
| MessageStream<QueryResponseMessage> responseStream = delegate.query(query, context); | ||
|
|
||
| // Collect responses for recording using reduce | ||
| List<QueryResponseMessage> responses = responseStream.reduce( |
There was a problem hiding this comment.
If the returned MessageStream is based on a Publisher/Flux, I think it will become an issue to collect everything.
| * @param expectedResult The expected query result. | ||
| * @return The current Then instance, for fluent interfacing. | ||
| */ | ||
| Query expectResult(@Nonnull Object expectedResult); |
There was a problem hiding this comment.
I am wondering whether the expect operations should be a reflection of the event and command versions. Query responses can be singular, plural, directly returned, async, or reactive. Whether that should be reflected is up for debate still. Differently put, we have not started the design sessions for this in the AF-team.
There was a problem hiding this comment.
Yes, I think this is the main issue to discuss here - how to support ALL our query types.
There was a problem hiding this comment.
@smcvb @MateuszNaKodach I hear you. I could do some research on how to better support reactive MessageStreams. As for the supporting of ALL query types I guess it would be beneficial to break it down what we would like the API to look like for different query types. For example I think it makes a lot of sense to have the API for subscriptionQueries to be:
given()
.subscriptionQuery(query)
.when()
.events(events)
.then()
.expect(initialResult(result)
.and()
.expectUpdate(update).
Which is completely different from the api in this PR, so I think it would be hard to group ALL query types under one single API. I would assume that the best course of action is to put this on hold until you guys have agreed on the future of this API. I'm sure some of the stuff in this PR is still usable either way.
|
Setting this PR to milestone 5.2.0, as we do not have sufficient time to round of this contribution before our foreseen release date of 5.1.0. |
Add Query Testing Support to AxonTestFixture
Overview
This PR adds query testing capabilities to
AxonTestFixture, enabling developers to test querieswith the same fluent given-when-then API used for command testing. The implementation includes a
deterministic auto-await mechanism that ensures read models are fully caught up before queries
execute to ensure results are reliable without putting the eventual consistency inside the developers head.
Problem Statement
Previously, testing queries in Axon Framework 5 required:
Thread.sleep) or manual awaits to wait for async event processingSolution
🎯 Deterministic Token-Based Auto-Await
The key innovation is a deterministic waiting mechanism that eliminates timing-related test failures:
How it works:
StreamingEventProcessorinstancesEventStore.latestToken()Duration.ZEROto skip entirely)📋 Complete API
Query Execution
Result Assertions
Exception Assertions
Example Usage
Architecture
New Components
RecordingQueryBusQuery bus decorator for testing that records all dispatched queries and their responses.
org.axonframework.test.fixturerecorded(),recordedQueries(),responsesOf()methodsreset()clears recorded queries between test phasesEventProcessorUtilsUtility class for waiting on event processors during testing.
org.axonframework.test.utilwaitForEventProcessorsToCatchUp(AxonConfiguration, Duration)AxonTestThenQueryImplements the
Then.Queryphase with result and exception assertions.AxonTestThenMessagefor event/command assertionsRecordingQueryBusfor future query assertion featuresPayloadMatcherfor deep equality checking.getCause())AxonTestWhen(Enhanced).query()methods to When phaseRecordingQueryBusto Then phaseSample Domain
Comprehensive test domain in
test/fixture/sampledomain:StudentReadModel- Read model recordGetStudentById- Query with nestedResultwrapperStudentRepository+InMemoryStudentRepository- Repository pattern withthread-safe implementation
StudentProjection- Event handler with@EventHandlerStudentQueryHandler- Query handler with@QueryHandlerStudentNotFoundException- Custom exceptionTest Coverage
Six comprehensive tests demonstrate all features:
givenEventsWhenQueryThenExpectResult_SuccessgivenEventsWhenQueryThenExpectResultSatisfies_SuccessgivenNoEventsWhenQueryThenExpectException_SuccessgivenNoEventsWhenQueryThenExpectExceptionWithMessage_SuccessgivenNoEventsWhenQueryThenExpectExceptionSatisfies_SuccessgivenEventsWhenQueryThenSuccess_SuccessAll tests use
PooledStreamingEventProcessorwith async processing to verify the deterministic mechanism.Breaking Changes
None. This is a pure addition to the existing
AxonTestFixtureAPI.Migration Guide
No migration needed. Existing tests continue to work unchanged. New query testing is opt-in.
Related Issues
#729
Commits
48e8731Implement deterministic query testing with token-based auto-await mechanisme90b19cImplement auto-await mechanism for query testing with delay-based approachd3558eaAdd query testing support to AxonTestFixture