Repository: AxonFramework/AxonFramework
Pull request #3366: [#3072] feat: Axon Framework 5 Test Fixture - remove Hamcrest from the interface + enhancements
Author: login: MateuszNaKodach
Labels: name: Type: Enhancement
description: Use to signal an issue enhances an already existing feature of the project.
name: Priority 1: Must
description: Highest priority. A release cannot be made if this issue isn’t resolved.
Created: 2025-04-03T14:18:48Z | Merged: 2025-04-08T13:25:51Z

+ fix bug with `events(List)` - there were varargs by mistake.
+ change `commands(List)` - behavior - execute each in dedicated Unit of Work.

TODO: 
In the next PRs Reporter will be replaced (not it's heavy based on Hamcrest), so I haven't added any methods to the Reporter, just placed in the fixture for now. 


Diff:
diff --git a/test/src/main/java/org/axonframework/test/AxonAssertionError.java b/test/src/main/java/org/axonframework/test/AxonAssertionError.java
index 4171984b0f5..4fb0f4f378c 100644
--- a/test/src/main/java/org/axonframework/test/AxonAssertionError.java
+++ b/test/src/main/java/org/axonframework/test/AxonAssertionError.java
@@ -16,7 +16,10 @@
 
 package org.axonframework.test;
 
+import jakarta.annotation.Nonnull;
+
 import java.util.Arrays;
+import java.util.Objects;
 
 /**
  * Error indication that an Assertion failed during a test case. The message of the error contains detailed information
@@ -32,14 +35,26 @@ public class AxonAssertionError extends AssertionError {
     /**
      * Create a new error instance using the given {@code detailMessage}.
      *
-     * @param detailMessage a detailed description of the failed assertion
+     * @param detailMessage A detailed description of the failed assertion.
      */
-    public AxonAssertionError(String detailMessage) {
+    public AxonAssertionError(@Nonnull String detailMessage) {
         super(detailMessage);
         StackTraceElement[] original = getStackTrace();
         setStackTrace(cleanStackTrace(original));
     }
 
+    /**
+     * Create a new error instance using the given {@code cause} and {@code detailMessage}.
+     *
+     * @param detailMessage A detailed description of the failed assertion.
+     * @param cause         The cause of the error.
+     */
+    public AxonAssertionError(@Nonnull String detailMessage, @Nonnull Throwable cause) {
+        super(Objects.requireNonNull(detailMessage), Objects.requireNonNull(cause));
+        StackTraceElement[] original = getStackTrace();
+        setStackTrace(cleanStackTrace(original));
+    }
+
     private StackTraceElement[] cleanStackTrace(StackTraceElement[] original) {
         int ignoreCount = 0;
         for (StackTraceElement element : original) {
diff --git a/test/src/main/java/org/axonframework/test/fixture/AxonTestFixture.java b/test/src/main/java/org/axonframework/test/fixture/AxonTestFixture.java
index ad3f0de7c46..5c81a843cf1 100644
--- a/test/src/main/java/org/axonframework/test/fixture/AxonTestFixture.java
+++ b/test/src/main/java/org/axonframework/test/fixture/AxonTestFixture.java
@@ -29,6 +29,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.function.UnaryOperator;
 
 /**
@@ -37,9 +39,9 @@
  * given-when-then style.
  *
  * @author Allard Buijze
- * @author Steven van Beelen
- * @author Mitchell Herrijgers
  * @author Mateusz Nowak
+ * @author Mitchell Herrijgers
+ * @author Steven van Beelen
  * @since 5.0.0
  */
 public class AxonTestFixture implements AxonTestPhase.Setup {
@@ -79,7 +81,7 @@ public static AxonTestFixture with(@Nonnull ApplicationConfigurer<?> configurer)
      * @return A new fixture instance
      */
     public static AxonTestFixture with(@Nonnull ApplicationConfigurer<?> configurer,
-                                           @Nonnull UnaryOperator<Customization> customization) {
+                                       @Nonnull UnaryOperator<Customization> customization) {
         Objects.requireNonNull(configurer, "Configurer may not be null");
         Objects.requireNonNull(customization, "Customization may not be null");
         var configuration = configurer
@@ -112,9 +114,17 @@ public Customization() {
 
         /**
          * Registers the given {@code fieldFilter}, which is used to define which Fields are used when comparing
-         * objects. The {@link org.axonframework.test.fixture.AxonTestPhase.Then.Event#events} and
-         * {@link org.axonframework.test.fixture.AxonTestPhase.Then.Command#resultMessage}, for example, use this
-         * filter.
+         * objects.
+         * <p>
+         * This filter is used by following methods:
+         * <ul>
+         *     <li>{@link AxonTestPhase.Then.Message#events}</li>
+         *     <li>{@link AxonTestPhase.Then.Message#commands}</li>
+         *     <li>{@link AxonTestPhase.Then.Command#resultMessagePayload}</li>
+         * </ul>
+         * <p>
+         * If you use custom assertions with methods like {@link AxonTestPhase.Then.Event#eventsSatisfy} or
+         * {@link AxonTestPhase.Then.Event#eventsMatch}  this filter is not taken into account.
          * <p/>
          * When multiple filters are registered, a Field must be accepted by all registered filters in order to be
          * accepted.
@@ -132,6 +142,16 @@ public Customization registerFieldFilter(@Nonnull FieldFilter fieldFilter) {
         /**
          * Indicates that a field with given {@code fieldName}, which is declared in given {@code declaringClass} is
          * ignored when performing deep equality checks.
+         * <p>
+         * This filter is used by following methods:
+         * <ul>
+         *     <li>{@link AxonTestPhase.Then.Message#events}</li>
+         *     <li>{@link AxonTestPhase.Then.Message#commands}</li>
+         *     <li>{@link AxonTestPhase.Then.Command#resultMessagePayload}</li>
+         * </ul>
+         * <p>
+         * If you use custom assertions with methods like {@link AxonTestPhase.Then.Event#eventsSatisfy} or
+         * {@link AxonTestPhase.Then.Event#eventsMatch}  this filter is not taken into account.
          *
          * @param declaringClass The class declaring the field.
          * @param fieldName      The name of the field.
diff --git a/test/src/main/java/org/axonframework/test/fixture/AxonTestGiven.java b/test/src/main/java/org/axonframework/test/fixture/AxonTestGiven.java
index b7ecadada2c..c27a7c32581 100644
--- a/test/src/main/java/org/axonframework/test/fixture/AxonTestGiven.java
+++ b/test/src/main/java/org/axonframework/test/fixture/AxonTestGiven.java
@@ -28,7 +28,6 @@
 import org.axonframework.messaging.unitofwork.AsyncUnitOfWork;
 import org.axonframework.messaging.unitofwork.ProcessingContext;
 
-import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Consumer;
@@ -77,8 +76,8 @@ private GenericEventMessage<Object> toGenericEventMessage(Object payload, MetaDa
     }
 
     @Override
-    public AxonTestPhase.Given events(@Nonnull List<?>... events) {
-        var messages = Arrays.stream(events)
+    public AxonTestPhase.Given events(@Nonnull List<?> events) {
+        var messages = events.stream()
                              .map(e -> e instanceof EventMessage<?> message
                                      ? message
                                      : toGenericEventMessage(e, MetaData.emptyInstance())
@@ -107,25 +106,36 @@ private void inUnitOfWorkOnInvocation(Function<ProcessingContext, CompletableFut
 
     @Override
     public AxonTestPhase.Given command(@Nonnull Object payload, @Nonnull MetaData metaData) {
+        var commandMessage = toGenericCommandMessage(payload, metaData);
+        return commands(commandMessage);
+    }
+
+    @Override
+    public AxonTestPhase.Given commands(@Nonnull List<?> commands) {
+        var messages = commands.stream()
+                               .map(c -> c instanceof CommandMessage<?> message
+                                       ? message
+                                       : toGenericCommandMessage(c, MetaData.emptyInstance())
+                               ).toArray(CommandMessage<?>[]::new);
+        return commands(messages);
+    }
+
+    private GenericCommandMessage<Object> toGenericCommandMessage(@Nonnull Object payload,
+                                                                  @Nonnull MetaData metaData
+    ) {
         var messageType = messageTypeResolver.resolve(payload);
-        var commandMessage = new GenericCommandMessage<>(
+        return new GenericCommandMessage<>(
                 messageType,
                 payload,
                 metaData
         );
-        return commands(commandMessage);
     }
 
     @Override
     public AxonTestPhase.Given commands(@Nonnull CommandMessage<?>... messages) {
-        inUnitOfWorkOnInvocation(processingContext -> {
-            CompletableFuture<? extends Message<?>> dispatchOneByOneFuture = CompletableFuture.completedFuture(null);
-            for (var message : messages) {
-                var dispatchFuture = commandBus.dispatch(message, processingContext);
-                dispatchOneByOneFuture = dispatchOneByOneFuture.thenCompose(m -> dispatchFuture);
-            }
-            return dispatchOneByOneFuture;
-        });
+        for (var message : messages) {
+            inUnitOfWorkOnInvocation(processingContext -> commandBus.dispatch(message, processingContext));
+        }
         return this;
     }
 
diff --git a/test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java b/test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
index 1824463839c..9313620179f 100644
--- a/test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
+++ b/test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
@@ -21,8 +21,8 @@
 import org.axonframework.commandhandling.CommandResultMessage;
 import org.axonframework.eventhandling.EventMessage;
 import org.axonframework.messaging.MetaData;
-import org.hamcrest.Matcher;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -44,10 +44,10 @@
  * </ul>
  * <p>
  * The test fixture manages {@link org.axonframework.messaging.unitofwork.AsyncUnitOfWork} instances during test execution,
- * automatically committing as appropriate. During the "given" phase, each operation (like {@link Given#event}},
+ * automatically committing as appropriate. During the Given phase, each operation (like {@link Given#event}},
  * {@link Given#command} or even batched like {@link Given#events} and {@link Given#commands})
- * is executed in its own separate {@link org.axonframework.messaging.unitofwork.AsyncUnitOfWork} that is committed immediately after execution. In the "when" phase, a single Unit of Work is started
- * and committed after the command is executed. The "then" phase only validates the results.
+ * is executed in its own separate {@link org.axonframework.messaging.unitofwork.AsyncUnitOfWork} that is committed immediately after execution. In the When phase, a single Unit of Work is started
+ * and committed after the command is executed. The Then phase only validates the results.
  * <p>
  * The test phases operates on components defined in {@link org.axonframework.configuration.NewConfiguration} that you pass to the fixture during its construction.
  * <p>
@@ -99,20 +99,20 @@ public interface AxonTestPhase {
      * Interface describing the initial setup phase of a test fixture. This phase allows configuring the test fixture
      * before starting the test execution.
      * <p>
-     * From this phase, you can transition to either the "given" phase to define initial state, or directly to the
-     * "when" phase if no prior state is needed.
+     * From this phase, you can transition to either the Given phase to define initial state, or directly to the When
+     * phase if no prior state is needed.
      */
     interface Setup {
 
         /**
-         * Transition to the "given" phase to define the initial state of the system before testing.
+         * Transition to the Given phase to define the initial state of the system before testing.
          *
          * @return A {@link Given} instance that allows defining the initial state.
          */
         AxonTestPhase.Given given();
 
         /**
-         * Transition directly to the "when" phase, skipping the "given" phase, which implies no prior state.
+         * Transition directly to the When phase, skipping the Given phase, which implies no prior state.
          *
          * @return A {@link When} instance that allows executing the test.
          */
@@ -120,12 +120,8 @@ interface Setup {
     }
 
     /**
-     * Interface describing the operations available in the "given" phase of the test fixture execution. This phase is
+     * Interface describing the operations available in the Given phase of the test fixture execution. This phase is
      * used to define the initial state of the system before executing the test action.
-     * <p>
-     * Each operation in the "given" phase (such as applying an event or dispatching a command) is executed in its own
-     * separate Unit of Work which is committed immediately after execution. This allows for building up the initial
-     * state incrementally with each operation being processed independently.
      */
     interface Given {
 
@@ -173,6 +169,9 @@ default Given event(@Nonnull Object payload, @Nonnull Map<String, ?> metaData) {
         /**
          * Configures the given {@code messages} as events in the "given" state. These events will be published in the
          * order they are provided.
+         * <p>
+         * All the {@code messages} will be processed within a single Unit of Work, meaning their processing won't be
+         * affected by changes made by earlier messages passed to this method.
          *
          * @param messages The event messages to publish.
          * @return The current Given instance, for fluent interfacing.
@@ -182,11 +181,28 @@ default Given event(@Nonnull Object payload, @Nonnull Map<String, ?> metaData) {
         /**
          * Configures the given {@code events} as events in the "given" state. These events will be published in the
          * order they are provided.
+         * <p>
+         * All the {@code messages} will be processed within a single Unit of Work, meaning their processing won't be
+         * affected by changes made by earlier messages passed to this method.
          *
          * @param events The lists of events to publish.
          * @return The current Given instance, for fluent interfacing.
          */
-        Given events(@Nonnull List<?>... events);
+        default Given events(@Nonnull Object... events) {
+            return events(Arrays.stream(events).toList());
+        }
+
+        /**
+         * Configures the given {@code events} as events in the "given" state. These events will be published in the
+         * order they are provided.
+         * <p>
+         * All the {@code messages} will be processed within a single Unit of Work, meaning their processing won't be
+         * affected by changes made by earlier messages passed to this method.
+         *
+         * @param events The lists of events to publish.
+         * @return The current Given instance, for fluent interfacing.
+         */
+        Given events(@Nonnull List<?> events);
 
         /**
          * Configures a single command with the given {@code payload} as part of the "given" state. This command will be
@@ -222,8 +238,11 @@ default Given command(@Nonnull Object payload, @Nonnull Map<String, ?> metaData)
         Given command(@Nonnull Object payload, @Nonnull MetaData metaData);
 
         /**
-         * Configures the given {@code messages} as commands in the "given" state. These commands will be dispatched in
-         * the order they are provided in the same Unit of Work.
+         * Configures the given {@code messages} as commands in the "given" state.
+         * <p>
+         * Each message will be processed in a dedicated Unit of Work, meaning that the processing of a message will be
+         * affected by the state changes made by the processing of previous messages. This behavior is in contrast to
+         * the {@link Given#events} method, where all messages are processed within a single Unit of Work.
          *
          * @param messages The command messages to dispatch.
          * @return The current Given instance, for fluent interfacing.
@@ -231,7 +250,35 @@ default Given command(@Nonnull Object payload, @Nonnull Map<String, ?> metaData)
         Given commands(@Nonnull CommandMessage<?>... messages);
 
         /**
-         * Transitions to the "when" phase to execute the test action.
+         * Configures the given {@code commands} as commands in the "given" state. These commands will be dispatched in
+         * the order they are provided in the same Unit of Work.
+         * <p>
+         * Each message will be processed in a dedicated Unit of Work, meaning that the processing of a message will be
+         * affected by the state changes made by the processing of previous messages. This behavior is in contrast to
+         * the {@link Given#events} method, where all messages are processed within a single Unit of Work.
+         *
+         * @param commands The command messages to dispatch.
+         * @return The current Given instance, for fluent interfacing.
+         */
+        default Given commands(@Nonnull Object... commands) {
+            return commands(Arrays.stream(commands).toList());
+        }
+
+        /**
+         * Configures the given {@code commands} as commands in the "given" state. These commands will be dispatched in
+         * the order they are provided in the same Unit of Work.
+         * <p>
+         * Each message will be processed in a dedicated Unit of Work, meaning that the processing of a message will be
+         * affected by the state changes made by the processing of previous messages. This behavior is in contrast to
+         * the {@link Given#events} method, where all messages are processed within a single Unit of Work.
+         *
+         * @param commands The command messages to dispatch.
+         * @return The current Given instance, for fluent interfacing.
+         */
+        Given commands(@Nonnull List<?> commands);
+
+        /**
+         * Transitions to the When phase to execute the test action.
          *
          * @return A {@link When} instance that allows executing the test.
          */
@@ -239,8 +286,8 @@ default Given command(@Nonnull Object payload, @Nonnull Map<String, ?> metaData)
     }
 
     /**
-     * Interface describing the operations available in the "when" phase of the test fixture execution. This phase is
-     * used to execute the actual action being tested, typically a command.
+     * Interface describing the operations available in the When phase of the test fixture execution. This phase is used
+     * to execute the actual action being tested, typically a command.
      * <p>
      * Each operation in the phase (such as dispatching a command) is executed in its own separate Unit of Work which is
      * committed immediately after execution. This allows for building up the initial state incrementally with each
@@ -251,7 +298,7 @@ interface When {
         interface Command {
 
             /**
-             * Transitions to the "then" phase to validate the results of the test.
+             * Transitions to the Then phase to validate the results of the test.
              *
              * @return A {@link Then} instance that allows validating the test results.
              */
@@ -261,7 +308,7 @@ interface Command {
         interface Event {
 
             /**
-             * Transitions to the "then" phase to validate the results of the test.
+             * Transitions to the Then phase to validate the results of the test.
              *
              * @return A {@link Then} instance that allows validating the test results.
              */
@@ -342,8 +389,8 @@ default Event event(@Nonnull Object payload) {
     }
 
     /**
-     * Interface describing the operations available in the "then" phase of the test fixture execution. This phase is
-     * used to validate the results of the test action executed in the "when" phase.
+     * Interface describing the operations available in the Then phase of the test fixture execution. This phase is used
+     * to validate the results of the test action executed in the When phase.
      */
     interface Then {
 
@@ -361,16 +408,13 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Invokes the given {@code consumer} of the command result message that has been returned during the When
+             * phase, allowing for <b>any</b> form of assertion.
              *
-             * @param matcher The matcher to verify the actual result message against.
+             * @param consumer Consumes the command result. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher);
+            Command resultMessageSatisfies(@Nonnull Consumer<? super CommandResultMessage<?>> consumer);
 
             /**
              * Expect the last command handler from the When phase to return the given {@code expectedPayload} after
@@ -385,39 +429,13 @@ interface Command extends Message<Command> {
             Command resultMessagePayload(@Nonnull Object expectedPayload);
 
             /**
-             * Expect the last command handler from the When phase to return a payload that matches the given
-             * {@code matcher} after execution.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
-             *
-             * @param matcher The matcher to verify the actual return value against.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            Command resultMessagePayloadMatching(@Nonnull Matcher<?> matcher);
-
-            /**
-             * Expect the given {@code expectedException} to occur during the When phase execution. The actual exception
-             * should be exactly of that type, subclasses are not accepted.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
-             *
-             * @param expectedException The type of exception expected from the When phase execution.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            Command exception(@Nonnull Class<? extends Throwable> expectedException);
-
-            /**
-             * Expect an exception to occur during the When phase that matches with the given {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Invokes the given {@code consumer} of the command result payload that has been returned during the When
+             * phase, allowing for <b>any</b> form of assertion.
              *
-             * @param matcher The matcher to validate the actual exception.
+             * @param consumer Consumes the command result payload. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command exception(@Nonnull Matcher<?> matcher);
+            Command resultMessagePayloadSatisfies(@Nonnull Consumer<Object> consumer);
         }
 
         /**
@@ -432,36 +450,11 @@ interface Event extends Message<Event> {
              * @return The current Then instance, for fluent interfacing.
              */
             Event success();
-
-            /**
-             * Expect the given {@code expectedException} to occur during the When phase execution. The actual exception
-             * should be exactly of that type, subclasses are not accepted.
-             * <p>
-             * Only take events into account that were published explicitly with the {@link When#event} or
-             * {@link When#events}. Hence, do not take into accounts events published as side effects of the message
-             * handlers.
-             *
-             * @param expectedException The type of exception expected from the When phase execution.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            Event exception(@Nonnull Class<? extends Throwable> expectedException);
-
-            /**
-             * Expect an exception to occur during the When phase that matches with the given {@code matcher}.
-             * <p>
-             * Only take events into account that were published explicitly with the {@link When#event} or
-             * {@link When#events}. Hence, do not take into accounts events published as side effects of the message
-             * handlers.
-             *
-             * @param matcher The matcher to validate the actual exception.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            Event exception(@Nonnull Matcher<?> matcher);
         }
 
         /**
-         * Interface describing the operations available in the "then" phase of the test fixture execution. It's
-         * possible to assert published messages from the When phase.
+         * Interface describing the operations available in the Then phase of the test fixture execution. It's possible
+         * to assert published messages from the When phase.
          *
          * @param <T> The type of the current Then instance, for fluent interfacing. The type depends on the operation
          *            which was triggered in the When phase.
@@ -496,31 +489,22 @@ interface Message<T extends Message<T>> {
             T events(@Nonnull EventMessage<?>... expectedEvents);
 
             /**
-             * Expect the published events during the {@link When} phase to match the given {@code matcher}.
-             * <p>
-             * Note: if no events were published, the matcher receives an empty List.
-             *
-             * @param matcher The matcher to match with the actually published events.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            T events(@Nonnull Matcher<? extends List<? super EventMessage<?>>> matcher);
-
-            /**
-             * Allow to consume the set of event messages which have been published during the "when" phase.
+             * Invokes the given {@code consumer} of the set of event messages that have been published during the When
+             * phase, allowing for <b>any</b> form of assertion.
              *
              * @param consumer Consumes the published events. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            T events(@Nonnull Consumer<List<? super EventMessage<?>>> consumer);
+            T eventsSatisfy(@Nonnull Consumer<List<EventMessage<?>>> consumer);
 
             /**
-             * Allow to check if the set of event messages which have been published during the "when" phase match given
-             * predicate.
+             * Allow to check if the set of event messages which have been published during the When phase match given
+             * {@code predicate}.
              *
              * @param predicate The predicate to check the dispatched events against.
              * @return The current Then instance, for fluent interfacing.
              */
-            T eventsMatch(@Nonnull Predicate<List<? super EventMessage<?>>> predicate);
+            T eventsMatch(@Nonnull Predicate<List<EventMessage<?>>> predicate);
 
             /**
              * Expect no events to have been published during the {@link When} phase.
@@ -532,7 +516,7 @@ default T noEvents() {
             }
 
             /**
-             * Expect the given set of commands to have been dispatched during the "when" phase.
+             * Expect the given set of commands to have been dispatched during the When phase.
              * <p>
              * All commands are compared for equality using a shallow equals comparison on all the fields of the
              * commands. This means that all assigned values on the commands' fields should have a proper equals
@@ -544,7 +528,7 @@ default T noEvents() {
             T commands(@Nonnull Object... expectedCommands);
 
             /**
-             * Expect the given set of command messages to have been dispatched during the "when" phase.
+             * Expect the given set of command messages to have been dispatched during the When phase.
              * <p>
              * All commands are compared for equality using a shallow equals comparison on all the fields of the
              * commands. This means that all assigned values on the commands' fields should have a proper equals
@@ -557,24 +541,25 @@ default T noEvents() {
             T commands(@Nonnull CommandMessage<?>... expectedCommands);
 
             /**
-             * Allow to consume the set of command messages which have been dispatched during the "when" phase.
+             * Invokes the given {@code consumer} of the set of command messages that have been dispatched during the
+             * When phase, allowing for <b>any</b> form of assertion. *
              *
              * @param consumer Consumes the dispatched commands. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            T commands(@Nonnull Consumer<List<? super CommandMessage<?>>> consumer);
+            T commandsSatisfy(@Nonnull Consumer<List<CommandMessage<?>>> consumer);
 
             /**
-             * Allow to check if the set of command messages which have been dispatched during the "when" phase match
+             * Allow to check if the set of command messages which have been dispatched during the When phase match
              * given predicate.
              *
              * @param predicate The predicate to check the dispatched commands against.
              * @return The current Then instance, for fluent interfacing.
              */
-            T commandsMatch(@Nonnull Predicate<List<? super CommandMessage<?>>> predicate);
+            T commandsMatch(@Nonnull Predicate<List<CommandMessage<?>>> predicate);
 
             /**
-             * Expect no command messages to have been dispatched during the "when" phase.
+             * Expect no command messages to have been dispatched during the When phase.
              *
              * @return The current Then instance, for fluent interfacing.
              */
@@ -607,6 +592,43 @@ default T noEvents() {
              *
              * @return a {@link Setup} instance that allows configuring a new test scenario.
              */
+
+            /**
+             * Expect the given {@code expectedException} to occur during the When phase execution. The actual exception
+             * should be exactly of that type, subclasses are not accepted.
+             * <p>
+             * Only take messages into account that were published explicitly with the {@link When#command} or
+             * {@link When#event}. Hence, do not take into accounts messages published as side effects of other message
+             * handlers present in the configuration.
+             *
+             * @param type The type of exception expected from the When phase execution.
+             * @return The current Then instance, for fluent interfacing.
+             */
+            T exception(@Nonnull Class<? extends Throwable> type);
+
+            /**
+             * Expect the exception with given {@code type} and {@code message} to occur during the When phase
+             * execution. The actual exception should be exactly of that type, subclasses are not accepted.
+             * <p>
+             * Only take messages into account that were published explicitly with the {@link When#command} or
+             * {@link When#event}. Hence, do not take into accounts messages published as side effects of other message
+             * handlers present in the configuration.
+             *
+             * @param type    The type of exception expected from the When phase execution.
+             * @param message The message of the exception expected from the When phase execution.
+             * @return The current Then instance, for fluent interfacing.
+             */
+            T exception(@Nonnull Class<? extends Throwable> type, @Nonnull String message);
+
+            /**
+             * Invokes the given {@code consumer} of the exception that has been throw during the When phase, allowing
+             * for <b>any</b> form of assertion.
+             *
+             * @param consumer Consumes the thrown exception. You may place your own assertions here.
+             * @return The current Then instance, for fluent interfacing.
+             */
+            T exceptionSatisfies(@Nonnull Consumer<Throwable> consumer);
+
             Setup and();
         }
     }
diff --git a/test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java b/test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java
index 2c61bbce6d3..dc94a3a3071 100644
--- a/test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java
+++ b/test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java
@@ -23,10 +23,9 @@
 import org.axonframework.test.aggregate.Reporter;
 import org.axonframework.test.matchers.PayloadMatcher;
 import org.hamcrest.CoreMatchers;
-import org.hamcrest.Matcher;
 import org.hamcrest.StringDescription;
 
-import static org.hamcrest.CoreMatchers.*;
+import java.util.function.Consumer;
 
 class AxonTestThenCommand
         extends AxonTestThenMessage<AxonTestPhase.Then.Command>
@@ -50,20 +49,21 @@ public AxonTestThenCommand(
 
     @Override
     public AxonTestPhase.Then.Command success() {
-        return resultMessage(anything());
+        return resultMessageSatisfies(c -> {
+        });
     }
 
     @Override
-    public AxonTestPhase.Then.Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher) {
-        if (matcher == null) {
-            return resultMessage(nullValue());
-        }
+    public AxonTestPhase.Then.Command resultMessageSatisfies(
+            @Nonnull Consumer<? super CommandResultMessage<?>> consumer) {
         StringDescription expectedDescription = new StringDescription();
-        matcher.describeTo(expectedDescription);
         if (actualException != null) {
             reporter.reportUnexpectedException(actualException, expectedDescription);
-        } else if (!matcher.matches(actualResult)) {
-            reporter.reportWrongResult(actualResult, expectedDescription);
+        }
+        try {
+            consumer.accept((CommandResultMessage<?>) actualResult);
+        } catch (AssertionError e) {
+            reporter.reportWrongResult(actualResult, "Result message to satisfy custom assertions: " + e.getMessage());
         }
         return this;
     }
@@ -87,35 +87,45 @@ public AxonTestPhase.Then.Command resultMessagePayload(@Nonnull Object expectedP
     }
 
     @Override
-    public AxonTestPhase.Then.Command resultMessagePayloadMatching(@Nonnull Matcher<?> matcher) {
-        if (matcher == null) {
-            return resultMessagePayloadMatching(nullValue());
-        }
+    public AxonTestPhase.Then.Command resultMessagePayloadSatisfies(@Nonnull Consumer<Object> consumer) {
         StringDescription expectedDescription = new StringDescription();
-        matcher.describeTo(expectedDescription);
         if (actualException != null) {
             reporter.reportUnexpectedException(actualException, expectedDescription);
-        } else if (!matcher.matches(actualResult.getPayload())) {
-            reporter.reportWrongResult(actualResult.getPayload(), expectedDescription);
+        }
+        try {
+            var payload = actualResult.getPayload();
+            consumer.accept(payload);
+        } catch (AssertionError e) {
+            reporter.reportWrongResult(actualResult.getPayload(),
+                                       "Result message to satisfy custom assertions: " + e.getMessage());
         }
         return this;
     }
 
     @Override
-    public AxonTestPhase.Then.Command exception(@Nonnull Class<? extends Throwable> expectedException) {
-        return exception(instanceOf(expectedException));
+    public AxonTestPhase.Then.Command exception(@Nonnull Class<? extends Throwable> type) {
+        StringDescription description = new StringDescription();
+        if (actualException == null) {
+            reporter.reportUnexpectedReturnValue(actualResult == null ? null : actualResult.getPayload(), description);
+        }
+        return super.exception(type);
     }
 
     @Override
-    public AxonTestPhase.Then.Command exception(@Nonnull Matcher<?> matcher) {
+    public AxonTestPhase.Then.Command exception(@Nonnull Class<? extends Throwable> type, @Nonnull String message) {
         StringDescription description = new StringDescription();
-        matcher.describeTo(description);
         if (actualException == null) {
             reporter.reportUnexpectedReturnValue(actualResult == null ? null : actualResult.getPayload(), description);
         }
-        if (!matcher.matches(actualException)) {
-            reporter.reportWrongException(actualException, description);
+        return super.exception(type, message);
+    }
+
+    @Override
+    public AxonTestPhase.Then.Command exceptionSatisfies(@Nonnull Consumer<Throwable> consumer) {
+        StringDescription description = new StringDescription();
+        if (actualException == null) {
+            reporter.reportUnexpectedReturnValue(actualResult == null ? null : actualResult.getPayload(), description);
         }
-        return this;
+        return super.exceptionSatisfies(consumer);
     }
 }
diff --git a/test/src/main/java/org/axonframework/test/fixture/AxonTestThenEvent.java b/test/src/main/java/org/axonframework/test/fixture/AxonTestThenEvent.java
index 92728199c78..42b57eb8efb 100644
--- a/test/src/main/java/org/axonframework/test/fixture/AxonTestThenEvent.java
+++ b/test/src/main/java/org/axonframework/test/fixture/AxonTestThenEvent.java
@@ -16,13 +16,12 @@
 
 package org.axonframework.test.fixture;
 
+import jakarta.annotation.Nonnull;
 import org.axonframework.configuration.NewConfiguration;
 import org.axonframework.messaging.MessageStream;
-import org.hamcrest.Matcher;
 import org.hamcrest.StringDescription;
-import org.jetbrains.annotations.NotNull;
 
-import static org.hamcrest.CoreMatchers.instanceOf;
+import java.util.function.Consumer;
 
 class AxonTestThenEvent
         extends AxonTestThenMessage<AxonTestPhase.Then.Event>
@@ -48,20 +47,29 @@ public AxonTestPhase.Then.Event success() {
     }
 
     @Override
-    public AxonTestPhase.Then.Event exception(@NotNull Class<? extends Throwable> expectedException) {
-        return exception(instanceOf(expectedException));
+    public AxonTestPhase.Then.Event exception(@Nonnull Class<? extends Throwable> type) {
+        StringDescription description = new StringDescription();
+        if (actualException == null) {
+            reporter.reportUnexpectedReturnValue(MessageStream.Empty.class.getSimpleName(), description);
+        }
+        return super.exception(type);
     }
 
     @Override
-    public AxonTestPhase.Then.Event exception(@NotNull Matcher<?> matcher) {
+    public AxonTestPhase.Then.Event exception(@Nonnull Class<? extends Throwable> type, @Nonnull String message) {
         StringDescription description = new StringDescription();
-        matcher.describeTo(description);
         if (actualException == null) {
             reporter.reportUnexpectedReturnValue(MessageStream.Empty.class.getSimpleName(), description);
         }
-        if (!matcher.matches(actualException)) {
-            reporter.reportWrongException(actualException, description);
+        return super.exception(type, message);
+    }
+
+    @Override
+    public AxonTestPhase.Then.Event exceptionSatisfies(@Nonnull Consumer<Throwable> consumer) {
+        StringDescription description = new StringDescription();
+        if (actualException == null) {
+            reporter.reportUnexpectedReturnValue(MessageStream.Empty.class.getSimpleName(), description);
         }
-        return this;
+        return super.exceptionSatisfies(consumer);
     }
 }
diff --git a/test/src/main/java/org/axonframework/test/fixture/AxonTestThenMessage.java b/test/src/main/java/org/axonframework/test/fixture/AxonTestThenMessage.java
index 985b61bedad..eee73ab954f 100644
--- a/test/src/main/java/org/axonframework/test/fixture/AxonTestThenMessage.java
+++ b/test/src/main/java/org/axonframework/test/fixture/AxonTestThenMessage.java
@@ -20,14 +20,13 @@
 import org.axonframework.commandhandling.CommandMessage;
 import org.axonframework.configuration.NewConfiguration;
 import org.axonframework.eventhandling.EventMessage;
+import org.axonframework.test.AxonAssertionError;
 import org.axonframework.test.aggregate.Reporter;
 import org.axonframework.test.matchers.MapEntryMatcher;
 import org.axonframework.test.matchers.MatchAllFieldFilter;
 import org.axonframework.test.matchers.Matchers;
 import org.axonframework.test.saga.CommandValidator;
-import org.hamcrest.Description;
 import org.hamcrest.Matcher;
-import org.hamcrest.StringDescription;
 
 import java.util.Arrays;
 import java.util.Iterator;
@@ -48,6 +47,7 @@ abstract class AxonTestThenMessage<T extends AxonTestPhase.Then.Message<T>>
     private final NewConfiguration configuration;
     private final AxonTestFixture.Customization customization;
     private final RecordingEventSink eventSink;
+    private final RecordingCommandBus commandBus;
 
     private final CommandValidator commandValidator;
     protected final Throwable actualException;
@@ -61,6 +61,7 @@ public AxonTestThenMessage(
     ) {
         this.configuration = configuration;
         this.customization = customization;
+        this.commandBus = commandBus;
         this.eventSink = eventSink;
         this.actualException = actualException;
         this.commandValidator = new CommandValidator(commandBus::recordedCommands,
@@ -104,32 +105,26 @@ public T events(@Nonnull EventMessage<?>... expectedEvents) {
     }
 
     @Override
-    public T events(@Nonnull Matcher<? extends List<? super EventMessage<?>>> matcher) {
+    public T eventsSatisfy(@Nonnull Consumer<List<EventMessage<?>>> consumer) {
         var publishedEvents = eventSink.recorded();
-        if (!matcher.matches(publishedEvents)) {
-            final Description expectation = new StringDescription();
-            matcher.describeTo(expectation);
-
-            final Description mismatch = new StringDescription();
-            matcher.describeMismatch(publishedEvents, mismatch);
-
-            reporter.reportWrongEvent(publishedEvents, expectation, mismatch, actualException);
+        try {
+            consumer.accept(publishedEvents);
+        } catch (AssertionError e) {
+            throw new AxonAssertionError("Events does not satisfy custom assertions", e);
         }
         return self();
     }
 
     @Override
-    public T events(@Nonnull Consumer<List<? super EventMessage<?>>> consumer) {
+    public T eventsMatch(@Nonnull Predicate<List<EventMessage<?>>> predicate) {
         var publishedEvents = eventSink.recorded();
-        consumer.accept(publishedEvents);
+        var result = predicate.test(publishedEvents);
+        if (!result) {
+            throw new AxonAssertionError("Events does not satisfy the predicate");
+        }
         return self();
     }
 
-    @Override
-    public T eventsMatch(@Nonnull Predicate<List<? super EventMessage<?>>> predicate) {
-        throw new UnsupportedOperationException("Not implemented yet");
-    }
-
     @Override
     public T commands(@Nonnull Object... expectedCommands) {
         commandValidator.assertDispatchedEqualTo(expectedCommands);
@@ -143,13 +138,24 @@ public T commands(@Nonnull CommandMessage<?>... expectedCommands) {
     }
 
     @Override
-    public T commands(@Nonnull Consumer<List<? super CommandMessage<?>>> consumer) {
-        throw new UnsupportedOperationException("Not implemented yet");
+    public T commandsSatisfy(@Nonnull Consumer<List<CommandMessage<?>>> consumer) {
+        var dispatchedCommands = commandBus.recordedCommands();
+        try {
+            consumer.accept(dispatchedCommands);
+        } catch (AssertionError e) {
+            throw new AxonAssertionError("Commands does not satisfy custom assertions", e);
+        }
+        return self();
     }
 
     @Override
-    public T commandsMatch(@Nonnull Predicate<List<? super CommandMessage<?>>> predicate) {
-        throw new UnsupportedOperationException("Not implemented yet");
+    public T commandsMatch(@Nonnull Predicate<List<CommandMessage<?>>> predicate) {
+        var dispatchedCommands = commandBus.recordedCommands();
+        var result = predicate.test(dispatchedCommands);
+        if (!result) {
+            throw new AxonAssertionError("Events does not satisfy the predicate");
+        }
+        return self();
     }
 
     @Override
@@ -158,6 +164,42 @@ public T noCommands() {
         return self();
     }
 
+    @Override
+    public T exceptionSatisfies(@Nonnull Consumer<Throwable> consumer) {
+        try {
+            consumer.accept(actualException);
+        } catch (AssertionError e) {
+            throw new AxonAssertionError("Exception does not satisfy custom assertions", e);
+        }
+        return self();
+    }
+
+    @Override
+    public T exception(@Nonnull Class<? extends Throwable> type, @Nonnull String message) {
+        if (actualException == null) {
+            throw new AxonAssertionError(
+                    "Expected exception of type " + type + " with message '" + message + "' but got none");
+        }
+        if (!type.isInstance(actualException) || !message.equals(actualException.getMessage())) {
+            throw new AxonAssertionError(
+                    "Expected " + type + " with message '" + message + "' but got " + actualException);
+        }
+        return self();
+    }
+
+    @Override
+    public T exception(@Nonnull Class<? extends Throwable> type) {
+        if (actualException == null) {
+            throw new AxonAssertionError(
+                    "Expected exception of type " + type + " but got none");
+        }
+        if (!type.isInstance(actualException)) {
+            throw new AxonAssertionError(
+                    "Expected " + type + " but got " + actualException);
+        }
+        return self();
+    }
+
     @SuppressWarnings("BooleanMethodIsAlwaysInverted")
     protected boolean verifyPayloadEquality(Object expectedPayload, Object actualPayload) {
         if (Objects.equals(expectedPayload, actualPayload)) {
diff --git a/test/src/main/java/org/axonframework/test/fixture/AxonTestWhen.java b/test/src/main/java/org/axonframework/test/fixture/AxonTestWhen.java
index 09b9b6b9bbe..3353d9b65ee 100644
--- a/test/src/main/java/org/axonframework/test/fixture/AxonTestWhen.java
+++ b/test/src/main/java/org/axonframework/test/fixture/AxonTestWhen.java
@@ -127,7 +127,7 @@ private void awaitCompletion(CompletableFuture<?> completion) {
             completion.join();
         } catch (Exception e) {
             this.actualResult = null;
-            this.actualException = e;
+            this.actualException = e.getCause();
         }
     }
 
diff --git a/test/src/main/java/org/axonframework/test/fixture/MessagesRecordingConfigurationEnhancer.java b/test/src/main/java/org/axonframework/test/fixture/MessagesRecordingConfigurationEnhancer.java
index cefc96fb11d..f29edd51ce1 100644
--- a/test/src/main/java/org/axonframework/test/fixture/MessagesRecordingConfigurationEnhancer.java
+++ b/test/src/main/java/org/axonframework/test/fixture/MessagesRecordingConfigurationEnhancer.java
@@ -16,16 +16,16 @@
 
 package org.axonframework.test.fixture;
 
+import jakarta.annotation.Nonnull;
 import org.axonframework.commandhandling.CommandBus;
 import org.axonframework.configuration.ConfigurationEnhancer;
 import org.axonframework.configuration.NewConfigurer;
 import org.axonframework.eventhandling.EventSink;
-import org.jetbrains.annotations.NotNull;
 
 class MessagesRecordingConfigurationEnhancer implements ConfigurationEnhancer {
 
     @Override
-    public void enhance(@NotNull NewConfigurer<?> configurer) {
+    public void enhance(@Nonnull NewConfigurer<?> configurer) {
         configurer.registerDecorator(EventSink.class,
                                      Integer.MAX_VALUE,
                                      (c, name, d) -> new RecordingEventSink(d))
diff --git a/test/src/main/java/org/axonframework/test/fixture/RecordingCommandBus.java b/test/src/main/java/org/axonframework/test/fixture/RecordingCommandBus.java
index 2f20d096d86..a6de46ea4b3 100644
--- a/test/src/main/java/org/axonframework/test/fixture/RecordingCommandBus.java
+++ b/test/src/main/java/org/axonframework/test/fixture/RecordingCommandBus.java
@@ -16,6 +16,8 @@
 
 package org.axonframework.test.fixture;
 
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
 import org.axonframework.commandhandling.CommandBus;
 import org.axonframework.commandhandling.CommandHandler;
 import org.axonframework.commandhandling.CommandMessage;
@@ -23,8 +25,6 @@
 import org.axonframework.messaging.Message;
 import org.axonframework.messaging.QualifiedName;
 import org.axonframework.messaging.unitofwork.ProcessingContext;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 
 import java.util.HashMap;
 import java.util.List;
@@ -41,7 +41,7 @@ public RecordingCommandBus(CommandBus delegate) {
     }
 
     @Override
-    public CompletableFuture<? extends Message<?>> dispatch(@NotNull CommandMessage<?> command,
+    public CompletableFuture<? extends Message<?>> dispatch(@Nonnull CommandMessage<?> command,
                                                             @Nullable ProcessingContext processingContext) {
         recorded.put(command, null);
         var commandResult = delegate.dispatch(command, processingContext);
@@ -53,12 +53,12 @@ public CompletableFuture<? extends Message<?>> dispatch(@NotNull CommandMessage<
     }
 
     @Override
-    public CommandBus subscribe(@NotNull QualifiedName name, @NotNull CommandHandler commandHandler) {
+    public CommandBus subscribe(@Nonnull QualifiedName name, @Nonnull CommandHandler commandHandler) {
         return delegate.subscribe(name, commandHandler);
     }
 
     @Override
-    public void describeTo(@NotNull ComponentDescriptor descriptor) {
+    public void describeTo(@Nonnull ComponentDescriptor descriptor) {
         descriptor.describeWrapperOf(delegate);
     }
 
diff --git a/test/src/main/java/org/axonframework/test/fixture/RecordingEventSink.java b/test/src/main/java/org/axonframework/test/fixture/RecordingEventSink.java
index 0d209b79fd7..c25548d63e5 100644
--- a/test/src/main/java/org/axonframework/test/fixture/RecordingEventSink.java
+++ b/test/src/main/java/org/axonframework/test/fixture/RecordingEventSink.java
@@ -16,9 +16,9 @@
 
 package org.axonframework.test.fixture;
 
+import jakarta.annotation.Nonnull;
 import org.axonframework.eventhandling.EventMessage;
 import org.axonframework.eventhandling.EventSink;
-import org.jetbrains.annotations.NotNull;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -38,15 +38,15 @@ class RecordingEventSink implements EventSink {
 
     @Override
     public void publish(
-            @NotNull ProcessingContext processingContext,
-            @NotNull List<EventMessage<?>> events
+            @Nonnull ProcessingContext processingContext,
+            @Nonnull List<EventMessage<?>> events
     ) {
         recorded.addAll(events);
         delegate.publish(processingContext, events);
     }
 
     @Override
-    public CompletableFuture<Void> publish(@NotNull List<EventMessage<?>> events) {
+    public CompletableFuture<Void> publish(@Nonnull List<EventMessage<?>> events) {
         return delegate.publish(events)
                        .thenRun(() -> recorded.addAll(events));
     }
diff --git a/test/src/test/java/org/axonframework/test/fixture/AxonTestFixtureMessagingTest.java b/test/src/test/java/org/axonframework/test/fixture/AxonTestFixtureMessagingTest.java
index 6946403b0ac..bd439c6ce31 100644
--- a/test/src/test/java/org/axonframework/test/fixture/AxonTestFixtureMessagingTest.java
+++ b/test/src/test/java/org/axonframework/test/fixture/AxonTestFixtureMessagingTest.java
@@ -28,12 +28,8 @@
 import org.axonframework.messaging.MetaData;
 import org.axonframework.messaging.QualifiedName;
 import org.axonframework.test.AxonAssertionError;
-import org.axonframework.test.aggregate.DeleteCommand;
-import org.axonframework.test.aggregate.MyAggregateDeletedEvent;
-import org.axonframework.test.aggregate.MyEvent;
 import org.axonframework.test.fixture.sampledomain.ChangeStudentNameCommand;
 import org.axonframework.test.fixture.sampledomain.StudentNameChangedEvent;
-import org.hamcrest.Matchers;
 import org.junit.jupiter.api.*;
 
 import java.util.ArrayList;
@@ -65,24 +61,6 @@ void givenNoPriorActivityWhenCommandThenExpectEvents() {
                    .events(studentNameChangedEventMessage("my-studentId-1", "name-1", 1));
         }
 
-        @Test
-        void assertionErrorIfThenNoEventsButThereAreSomeEvents() {
-            var configurer = MessagingConfigurer.create();
-            registerChangeStudentNameHandlerReturnsEmpty(configurer);
-
-            var fixture = AxonTestFixture.with(configurer);
-
-            var assertionError = assertThrows(AxonAssertionError.class, () ->
-                    fixture.given()
-                           .noPriorActivity()
-                           .when()
-                           .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
-                           .then()
-                           .noEvents()
-            );
-            assertTrue(assertionError.getMessage().contains("The published events do not match the expected events"));
-        }
-
         @Test
         void givenNothingWhenCommandThenExpectEvents() {
             var configurer = MessagingConfigurer.create();
@@ -97,24 +75,6 @@ void givenNothingWhenCommandThenExpectEvents() {
                    .events(studentNameChangedEventMessage("my-studentId-1", "name-1", 1));
         }
 
-        @Test
-        void assertionErrorIfThenExpectEventsButWithDifferentPayload() {
-            var configurer = MessagingConfigurer.create();
-            registerChangeStudentNameHandlerReturnsEmpty(configurer);
-
-            var fixture = AxonTestFixture.with(configurer);
-
-            var assertionError = assertThrows(AxonAssertionError.class, () ->
-                    fixture.given()
-                           .when()
-                           .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
-                           .then()
-                           .events(studentNameChangedEventMessage("my-studentId-1", "name-2", 1))
-            );
-            assertTrue(assertionError.getMessage()
-                                     .contains("One of the messages contained a different payload than expected"));
-        }
-
         @Test
         void whenCommandThenExpectEvents() {
             var configurer = MessagingConfigurer.create();
@@ -152,7 +112,7 @@ void whenCommandReturnsEmptyThenSuccessWithNullValue() {
                    .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
                    .then()
                    .success()
-                   .resultMessage(Matchers.nullValue());
+                   .resultMessageSatisfies(Assertions::assertNull);
         }
 
         @Test
@@ -169,6 +129,23 @@ void whenCommandReturnsSingleThenSuccessWithValue() {
                    .resultMessagePayload(new CommandResult("Result name-1"));
         }
 
+        @Test
+        void whenCommandReturnsSingleThenSuccessWithValueConsumer() {
+            var configurer = MessagingConfigurer.create();
+            registerChangeStudentNameHandlerReturnsSingle(configurer);
+
+            var fixture = AxonTestFixture.with(configurer);
+
+            fixture.when()
+                   .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
+                   .then()
+                   .resultMessagePayloadSatisfies(result -> {
+                       var payload = (CommandResult) result;
+                       assertEquals("Result name-1", payload.message());
+                       assertNull(payload.metadataSample());
+                   }).success();
+        }
+
         @Test
         void whenCommandWithMetaDataThenSuccessWithTheMetaData() {
             var configurer = MessagingConfigurer.create();
@@ -198,6 +175,21 @@ void whenCommandWithMetaDataMapThenSuccessWithTheMetaData() {
                    .resultMessagePayload(new CommandResult("Result name-1", "metaValue"));
         }
 
+        @Test
+        void whenCommandWithMetaDataMapThenSuccessWithTheMetaDataSatisfies() {
+            var configurer = MessagingConfigurer.create();
+            registerChangeStudentNameHandlerReturnsSingle(configurer);
+
+            var fixture = AxonTestFixture.with(configurer);
+
+            fixture.when()
+                   .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"), Map.of("sample", "metaValue"))
+                   .then()
+                   .success()
+                   .resultMessageSatisfies(c -> assertEquals(c.getPayload(),
+                                                             new CommandResult("Result name-1", "metaValue")));
+        }
+
         @Test
         void givenEventsWhenCommandReturnsSingleThenSuccessWithValue() {
             var configurer = MessagingConfigurer.create();
@@ -269,59 +261,126 @@ void givenNoPriorActivityWhenCommandThenExpectException() {
                    .when()
                    .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
                    .then()
-                   .exception(RuntimeException.class);
+                   .exception(RuntimeException.class)
+                   .exceptionSatisfies(thrown -> assertEquals("Test", thrown.getMessage()));
         }
 
         @Test
-        void assertionErrorIfExpectExceptionButSuccess() {
+        void givenNoPriorActivityWhenCommandHandlerThrowsThenExpectException() {
             var configurer = MessagingConfigurer.create();
             configurer.registerDecorator(
                     CommandBus.class,
                     0,
                     (c, n, d) -> d.subscribe(
                             new QualifiedName(ChangeStudentNameCommand.class),
-                            (command, context) -> MessageStream.empty().cast()
-                    )
+                            (command, context) -> {
+                                throw new RuntimeException("Simulated exception");
+                            })
             );
 
             var fixture = AxonTestFixture.with(configurer);
 
-            var assertionError = assertThrows(AxonAssertionError.class, () ->
-                    fixture.given()
-                           .noPriorActivity()
-                           .when()
-                           .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
-                           .then()
-                           .exception(RuntimeException.class)
-            );
-            assertTrue(assertionError.getMessage()
-                                     .contains("The message handler returned normally, but an exception was expected"));
+            fixture.given()
+                   .noPriorActivity()
+                   .when()
+                   .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
+                   .then()
+                   .exception(RuntimeException.class)
+                   .exceptionSatisfies(thrown -> assertEquals("Simulated exception", thrown.getMessage()));
         }
 
-
-        @Test
-        void assertionErrorIfExpectSuccessButException() {
-            var configurer = MessagingConfigurer.create();
-            configurer.registerDecorator(
-                    CommandBus.class,
-                    0,
-                    (c, n, d) -> d.subscribe(
-                            new QualifiedName(ChangeStudentNameCommand.class),
-                            (command, context) -> MessageStream.failed(new RuntimeException("Test"))
-                    )
-            );
-
-            var fixture = AxonTestFixture.with(configurer);
-
-            var assertionError = assertThrows(AxonAssertionError.class, () ->
-                    fixture.given()
-                           .noPriorActivity()
-                           .when()
-                           .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
-                           .then()
-                           .success()
-            );
-            assertTrue(assertionError.getMessage().contains("The message handler threw an unexpected exception"));
+        @Nested
+        class AssertionErrors {
+
+            @Test
+            void ifThenNoEventsButThereAreSomeEvents() {
+                var configurer = MessagingConfigurer.create();
+                registerChangeStudentNameHandlerReturnsEmpty(configurer);
+
+                var fixture = AxonTestFixture.with(configurer);
+
+                var assertionError = assertThrows(AxonAssertionError.class, () ->
+                        fixture.given()
+                               .noPriorActivity()
+                               .when()
+                               .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
+                               .then()
+                               .noEvents()
+                );
+                assertTrue(assertionError.getMessage()
+                                         .contains("The published events do not match the expected events"));
+            }
+
+
+            @Test
+            void ifThenExpectEventsButWithDifferentPayload() {
+                var configurer = MessagingConfigurer.create();
+                registerChangeStudentNameHandlerReturnsEmpty(configurer);
+
+                var fixture = AxonTestFixture.with(configurer);
+
+                var assertionError = assertThrows(AxonAssertionError.class, () ->
+                        fixture.given()
+                               .when()
+                               .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
+                               .then()
+                               .events(studentNameChangedEventMessage("my-studentId-1", "name-2", 1))
+                );
+                assertTrue(assertionError.getMessage()
+                                         .contains("One of the messages contained a different payload than expected"));
+            }
+
+            @Test
+            void ifExpectExceptionButSuccess() {
+                var configurer = MessagingConfigurer.create();
+                configurer.registerDecorator(
+                        CommandBus.class,
+                        0,
+                        (c, n, d) -> d.subscribe(
+                                new QualifiedName(ChangeStudentNameCommand.class),
+                                (command, context) -> MessageStream.empty().cast()
+                        )
+                );
+
+                var fixture = AxonTestFixture.with(configurer);
+
+                var assertionError = assertThrows(AxonAssertionError.class, () ->
+                        fixture.given()
+                               .noPriorActivity()
+                               .when()
+                               .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
+                               .then()
+                               .exception(RuntimeException.class)
+                );
+                assertTrue(assertionError.getMessage()
+                                         .contains(
+                                                 "The message handler returned normally, but an exception was expected"));
+            }
+
+            @Test
+            void ifExpectSuccessButException() {
+                var configurer = MessagingConfigurer.create();
+                configurer.registerDecorator(
+                        CommandBus.class,
+                        0,
+                        (c, n, d) -> d.subscribe(
+                                new QualifiedName(ChangeStudentNameCommand.class),
+                                (command, context) -> MessageStream.failed(new RuntimeException("Test"))
+                        )
+                );
+
+                var fixture = AxonTestFixture.with(configurer);
+
+                var assertionError = assertThrows(AxonAssertionError.class, () ->
+                        fixture.given()
+                               .noPriorActivity()
+                               .when()
+                               .command(new ChangeStudentNameCommand("my-studentId-1", "name-1"))
+                               .then()
+                               .success()
+                );
+                assertTrue(assertionError.getMessage().contains("The message handler threw an unexpected exception"));
+            }
         }
     }
 
@@ -350,35 +409,45 @@ void whenEventThenExpectedCommand() {
         }
 
         @Test
-        void whenEventHandlerFailsThenException() {
+        void whenEventThenExpectedCommandCustomAssertion() {
             var configurer = MessagingConfigurer.create();
-            configurer.registerEventSink(c -> (events) -> CompletableFuture.failedFuture(new RuntimeException(
-                    "Simulated failure")));
+            AtomicBoolean eventHandled = new AtomicBoolean(false);
+            registerChangeStudentNameHandlerReturnsSingle(configurer);
+            configurer.registerEventSink(c -> (events) -> {
+                if (!eventHandled.getAndSet(true)) {
+                    var commandGateway = c.getComponent(CommandGateway.class);
+                    commandGateway.sendAndWait(new ChangeStudentNameCommand("id", "name"));
+                }
+                return CompletableFuture.completedFuture(null);
+            });
 
             var fixture = AxonTestFixture.with(configurer);
 
             fixture.when()
                    .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
                    .then()
-                   .exception(RuntimeException.class)
-                   .noCommands();
+                   .commandsSatisfy(commands -> {
+                       assertEquals(1, commands.size());
+                       var command = commands.getFirst();
+                       assertEquals("id", ((ChangeStudentNameCommand) command.getPayload()).id());
+                       assertEquals("name", ((ChangeStudentNameCommand) command.getPayload()).name());
+                   }).commandsMatch(commands -> !commands.isEmpty())
+                   .success();
         }
 
         @Test
-        void assertionErrorIfExpectSuccessButException() {
+        void whenEventHandlerFailsThenException() {
             var configurer = MessagingConfigurer.create();
             configurer.registerEventSink(c -> (events) -> CompletableFuture.failedFuture(new RuntimeException(
                     "Simulated failure")));
 
             var fixture = AxonTestFixture.with(configurer);
 
-            var assertionError = assertThrows(AxonAssertionError.class, () ->
-                    fixture.when()
-                           .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
-                           .then()
-                           .success()
-            );
-            assertTrue(assertionError.getMessage().contains("The message handler threw an unexpected exception"));
+            fixture.when()
+                   .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
+                   .then()
+                   .exception(RuntimeException.class)
+                   .noCommands();
         }
 
         @Test
@@ -391,8 +460,8 @@ void whenEventHandlerDoesNotFailThenSuccess() {
             fixture.when()
                    .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
                    .then()
-                   .success()
-                   .noCommands();
+                   .noCommands()
+                   .success();
         }
 
         @Test
@@ -413,21 +482,61 @@ void whenEventsHandlerDoesNotFailThenSuccess() {
                    .noCommands();
         }
 
-        @Test
-        void assertionErrorIfExpectExceptionButSuccess() {
-            var configurer = MessagingConfigurer.create();
-            configurer.registerEventSink(c -> (events) -> CompletableFuture.completedFuture(null));
-
-            var fixture = AxonTestFixture.with(configurer);
-
-            var assertionError = assertThrows(AxonAssertionError.class, () ->
-                    fixture.when()
-                           .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
-                           .then()
-                           .exception(RuntimeException.class)
-            );
-            assertTrue(assertionError.getMessage()
-                                     .contains("The message handler returned normally, but an exception was expected"));
+        @Nested
+        class AssertionErrors {
+
+            @Test
+            void ifExpectSuccessButException() {
+                var configurer = MessagingConfigurer.create();
+                configurer.registerEventSink(c -> (events) -> CompletableFuture.failedFuture(new RuntimeException(
+                        "Simulated failure")));
+
+                var fixture = AxonTestFixture.with(configurer);
+
+                var assertionError = assertThrows(AxonAssertionError.class, () ->
+                        fixture.when()
+                               .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
+                               .then()
+                               .success()
+                );
+                assertTrue(assertionError.getMessage().contains("The message handler threw an unexpected exception"));
+            }
+
+            @Test
+            void ifExpectExceptionButSuccess() {
+                var configurer = MessagingConfigurer.create();
+                configurer.registerEventSink(c -> (events) -> CompletableFuture.completedFuture(null));
+
+                var fixture = AxonTestFixture.with(configurer);
+
+                var assertionError = assertThrows(AxonAssertionError.class, () ->
+                        fixture.when()
+                               .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
+                               .then()
+                               .exception(RuntimeException.class)
+                );
+                assertTrue(assertionError.getMessage()
+                                         .contains(
+                                                 "The message handler returned normally, but an exception was expected"));
+            }
+
+            @Test
+            void whenEventHandlerFailsThenException() {
+                var configurer = MessagingConfigurer.create();
+                configurer.registerEventSink(c -> (events) -> CompletableFuture.failedFuture(new IllegalStateException(
+                        "Simulated failure")));
+
+                var fixture = AxonTestFixture.with(configurer);
+
+                var assertionError = assertThrows(AxonAssertionError.class, () ->
+                        fixture.when()
+                               .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
+                               .then()
+                               .exception(IllegalStateException.class, "Simulated exception")
+                );
+                assertTrue(assertionError.getMessage().contains(
+                        "Expected class java.lang.IllegalStateException with message 'Simulated exception' but got java.lang.IllegalStateException: Simulated failure"));
+            }
         }
     }
 
diff --git a/test/src/test/java/org/axonframework/test/fixture/AxonTestFixtureStatefulCommandHandlerTest.java b/test/src/test/java/org/axonframework/test/fixture/AxonTestFixtureStatefulCommandHandlerTest.java
index 8a054f80603..091fffc828d 100644
--- a/test/src/test/java/org/axonframework/test/fixture/AxonTestFixtureStatefulCommandHandlerTest.java
+++ b/test/src/test/java/org/axonframework/test/fixture/AxonTestFixtureStatefulCommandHandlerTest.java
@@ -38,6 +38,7 @@
 import org.axonframework.test.fixture.sampledomain.StudentNameChangedEvent;
 import org.junit.jupiter.api.*;
 
+import java.util.List;
 import java.util.Objects;
 
 import static org.junit.jupiter.api.Assertions.*;
@@ -76,7 +77,10 @@ void givenEventsWhenCommandThenNoEventsConsumer() {
                .when()
                .command(changeToTheSameName)
                .then()
-               .events(events -> assertTrue(events.isEmpty()));
+               .noEvents()
+               .eventsSatisfy(events -> assertTrue(events.isEmpty()))
+               .eventsMatch(List::isEmpty)
+               .success();
     }
 
     @Test
@@ -113,6 +117,58 @@ void givenEventAndCommandWhenCommandThenEvents() {
                .events(new StudentNameChangedEvent("my-studentId-1", "name-3", 3));
     }
 
+    @Test
+    void givenMultipleEventsWhenCommandThenEvents() {
+        var configurer = MessagingConfigurer.create();
+        registerSampleStatefulCommandHandler(configurer);
+
+        var fixture = AxonTestFixture.with(configurer);
+
+        fixture.given()
+               .event(new StudentNameChangedEvent("my-studentId-1", "name-1", 1))
+               .event(new StudentNameChangedEvent("my-studentId-1", "name-2", 2))
+               .when()
+               .command(new ChangeStudentNameCommand("my-studentId-1", "name-3"))
+               .then()
+               .events(new StudentNameChangedEvent("my-studentId-1", "name-3", 3));
+    }
+
+    @Test
+    void givenEventsListWhenCommandThenEvents() {
+        var configurer = MessagingConfigurer.create();
+        registerSampleStatefulCommandHandler(configurer);
+
+        var fixture = AxonTestFixture.with(configurer);
+
+        var givenEventsList = List.of(
+                new StudentNameChangedEvent("my-studentId-1", "name-1", 1),
+                new StudentNameChangedEvent("my-studentId-1", "name-2", 2));
+        fixture.given()
+               .events(givenEventsList)
+               .when()
+               .command(new ChangeStudentNameCommand("my-studentId-1", "name-3"))
+               .then()
+               .events(new StudentNameChangedEvent("my-studentId-1", "name-3", 3));
+    }
+
+    @Test
+    void givenCommandsListWhenCommandThenEvents() {
+        var configurer = MessagingConfigurer.create();
+        registerSampleStatefulCommandHandler(configurer);
+
+        var fixture = AxonTestFixture.with(configurer);
+
+        var givenCommandsList = List.of(
+                new ChangeStudentNameCommand("my-studentId-1", "name-1"),
+                new ChangeStudentNameCommand("my-studentId-1", "name-2"));
+        fixture.given()
+               .commands(givenCommandsList)
+               .when()
+               .command(new ChangeStudentNameCommand("my-studentId-1", "name-3"))
+               .then()
+               .events(new StudentNameChangedEvent("my-studentId-1", "name-3", 3));
+    }
+
     @Test
     void givenEventsWhenCommandThenExpectEvents() {
         var configurer = MessagingConfigurer.create();
@@ -179,8 +235,6 @@ void chainSameFixturePhaseTwice() {
                 .then()
                 .noEvents();
         }
-
-
     }
 
     private static void registerSampleStatefulCommandHandler(MessagingConfigurer configurer) {


Reviews:
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T10:28:35Z
user: login: CodeDrivenMitch
body: Looks pretty good, but I have some naming comments. In addition, we need to check if we can specify project-wide what the default non-null annotation is. Otherwise anytime you implement a method via IntelliJ, you get the wrong annotation
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T13:11:43Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T13:47:31Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T13:49:40Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T13:55:09Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T13:55:29Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T14:10:15Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T14:19:24Z
user: login: smcvb
body: Bunch of JavaDoc remarks, and a question on the reporter message we share whenever the `consumer` containing the assert invocations fails. Not major, but still important enough to withhold approval of this PR (at least to me).
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T10:07:06Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T12:45:02Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:01:56Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:04:59Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T13:10:10Z
user: login: CodeDrivenMitch
body: Some small comments, but otherwise looks good to me. Nice work :)
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T09:26:27Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T10:05:33Z
user: login: MateuszNaKodach
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T12:42:44Z

Review comments:
diff_hunk: @@ -495,23 +448,13 @@ interface Message<T extends Message<T>> {
              */
             T events(@Nonnull EventMessage<?>... expectedEvents);
 
-            /**
-             * Expect the published events during the {@link When} phase to match the given {@code matcher}.
-             * <p>
-             * Note: if no events were published, the matcher receives an empty List.
-             *
-             * @param matcher The matcher to match with the actually published events.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            T events(@Nonnull Matcher<? extends List<? super EventMessage<?>>> matcher);
-
             /**
              * Allow to consume the set of event messages which have been published during the "when" phase.
              *
              * @param consumer Consumes the published events. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            T events(@Nonnull Consumer<List<? super EventMessage<?>>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: MateuszNaKodach
body: IDE hints are better in this case, before default type here was "Object" whie using lambda notation without specifying the parameter type. I think it's the most common use case. If someone really want inherited type, they may cast inside.
created_at: 2025-04-04T10:28:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -40,6 +40,18 @@ public AxonAssertionError(String detailMessage) {
         setStackTrace(cleanStackTrace(original));
     }
 
+    /**
+     * Create a new error instance using the given {@code cause} and {@code detailMessage}.
+     *
+     * @param cause         the cause of the error
+     * @param detailMessage a detailed description of the failed assertion
+     */
+    public AxonAssertionError(String detailMessage, Throwable cause) {
path: test/src/main/java/org/axonframework/test/AxonAssertionError.java
user: login: CodeDrivenMitch
body: Non-null annotations? :) 
created_at: 2025-04-04T12:57:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +27,7 @@
 import org.axonframework.messaging.MetaData;
 import org.axonframework.messaging.unitofwork.AsyncUnitOfWork;
 import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
path: test/src/main/java/org/axonframework/test/fixture/AxonTestGiven.java
user: login: CodeDrivenMitch
body: This is not the right annotation. You need `Nonnull` from jakarta
created_at: 2025-04-04T12:57:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -495,23 +448,13 @@ interface Message<T extends Message<T>> {
              */
             T events(@Nonnull EventMessage<?>... expectedEvents);
 
-            /**
-             * Expect the published events during the {@link When} phase to match the given {@code matcher}.
-             * <p>
-             * Note: if no events were published, the matcher receives an empty List.
-             *
-             * @param matcher The matcher to match with the actually published events.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            T events(@Nonnull Matcher<? extends List<? super EventMessage<?>>> matcher);
-
             /**
              * Allow to consume the set of event messages which have been published during the "when" phase.
              *
              * @param consumer Consumes the published events. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            T events(@Nonnull Consumer<List<? super EventMessage<?>>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: CodeDrivenMitch
body: Agreed
created_at: 2025-04-04T12:58:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,20 +54,20 @@ public AxonTestThenCommand(
 
     @Override
     public AxonTestPhase.Then.Command success() {
-        return resultMessage(anything());
+        return resultMessage(c -> {
+        });
     }
 
     @Override
-    public AxonTestPhase.Then.Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher) {
-        if (matcher == null) {
-            return resultMessage(nullValue());
-        }
+    public AxonTestPhase.Then.Command resultMessage(@NotNull Consumer<? super CommandResultMessage<?>> consumer) {
path: test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java
user: login: CodeDrivenMitch
body: Shouldn't we remove the `? super` here as well?
created_at: 2025-04-04T12:59:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,6 +30,7 @@
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.StringDescription;
+import org.jetbrains.annotations.NotNull;
path: test/src/main/java/org/axonframework/test/fixture/AxonTestThenMessage.java
user: login: CodeDrivenMitch
body:  Same thing, wrong non-null annotation
created_at: 2025-04-04T13:00:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -186,7 +185,7 @@ default Given event(@Nonnull Object payload, @Nonnull Map<String, ?> metaData) {
          * @param events The lists of events to publish.
          * @return The current Given instance, for fluent interfacing.
          */
-        Given events(@Nonnull List<?>... events);
+        Given events(@Nonnull List<?> events);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: CodeDrivenMitch
body: It would be nice to overload this method to be able to have `Object... events`
created_at: 2025-04-04T13:02:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,16 +369,12 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result of command message which have been published during the "when" phase.
              *
-             * @param matcher The matcher to verify the actual result message against.
+             * @param consumer Consumes the command result. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher);
+            Command resultMessage(@Nonnull Consumer<? super CommandResultMessage<?>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: CodeDrivenMitch
body: I am not 100% sure about this signature.

I think I would like to include `asserting` in the method name, to make it clear that the user has to make its own assertions. Such as `assertingResultMessage`, or maybe `resultMessageMatches`
created_at: 2025-04-04T13:03:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,16 +369,12 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result of command message which have been published during the "when" phase.
              *
-             * @param matcher The matcher to verify the actual result message against.
+             * @param consumer Consumes the command result. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher);
+            Command resultMessage(@Nonnull Consumer<? super CommandResultMessage<?>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: CodeDrivenMitch
body: Of course, this pointer would go for the other methods
created_at: 2025-04-04T13:04:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,16 +369,12 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result of command message which have been published during the "when" phase.
              *
-             * @param matcher The matcher to verify the actual result message against.
+             * @param consumer Consumes the command result. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher);
+            Command resultMessage(@Nonnull Consumer<? super CommandResultMessage<?>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: CodeDrivenMitch
body: I see you did `eventsMatch` for events, because it takes a `Predicate`. Something similar, but for assert consumers.

We could also maybe make a functional interface, so it shows in the signature: `resultMessage(AssertionFunction<CommandResultMessage<?>>`
created_at: 2025-04-04T13:05:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,16 +369,12 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result of command message which have been published during the "when" phase.
              *
-             * @param matcher The matcher to verify the actual result message against.
+             * @param consumer Consumes the command result. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher);
+            Command resultMessage(@Nonnull Consumer<? super CommandResultMessage<?>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: CodeDrivenMitch
body: Ahhh, sorry for the quadruple ocmment, but I just recalled assertJ uses `satisfy` for this:

So `resultMessageSatifies(AssertionFunction<CommandResultMessage<?>>)`. What do you think?
created_at: 2025-04-04T13:09:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -27,6 +27,7 @@
 import org.axonframework.messaging.MetaData;
 import org.axonframework.messaging.unitofwork.AsyncUnitOfWork;
 import org.axonframework.messaging.unitofwork.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
path: test/src/main/java/org/axonframework/test/fixture/AxonTestGiven.java
user: login: MateuszNaKodach
body: It's a trap! :) Of the IntelliJ automation.
created_at: 2025-04-04T13:47:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,20 +54,20 @@ public AxonTestThenCommand(
 
     @Override
     public AxonTestPhase.Then.Command success() {
-        return resultMessage(anything());
+        return resultMessage(c -> {
+        });
     }
 
     @Override
-    public AxonTestPhase.Then.Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher) {
-        if (matcher == null) {
-            return resultMessage(nullValue());
-        }
+    public AxonTestPhase.Then.Command resultMessage(@NotNull Consumer<? super CommandResultMessage<?>> consumer) {
path: test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java
user: login: MateuszNaKodach
body: Interesting... I don't know why this one works OK. I need to compare those situations. 
Hmm....
![image](https://github.com/user-attachments/assets/f6347403-d93b-4768-ac71-aaf38a5f87c2)

created_at: 2025-04-04T13:49:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,16 +369,12 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result of command message which have been published during the "when" phase.
              *
-             * @param matcher The matcher to verify the actual result message against.
+             * @param consumer Consumes the command result. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher);
+            Command resultMessage(@Nonnull Consumer<? super CommandResultMessage<?>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: MateuszNaKodach
body: I was also thinking about that.
I prefer to have just `resultMessage`, but as you said I've done it because of the predicate. 
But maybe it'd better to rename all assertion methods like:
`eventsSatisfy(Consumer)`
`eventsMatch(Predicate)`
`resultMessageSatifies(Consumer)`
`resultMessageMatch(Predicate)`

WDYT?
created_at: 2025-04-04T13:55:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,16 +369,12 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result of command message which have been published during the "when" phase.
              *
-             * @param matcher The matcher to verify the actual result message against.
+             * @param consumer Consumes the command result. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher);
+            Command resultMessage(@Nonnull Consumer<? super CommandResultMessage<?>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: MateuszNaKodach
body: `resultMessageSatifies(Consumer<CommandResultMessage<?>>)` is enough? Or do you think there are some advantages of dedicated interface? 
created_at: 2025-04-04T13:55:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -186,7 +185,7 @@ default Given event(@Nonnull Object payload, @Nonnull Map<String, ?> metaData) {
          * @param events The lists of events to publish.
          * @return The current Given instance, for fluent interfacing.
          */
-        Given events(@Nonnull List<?>... events);
+        Given events(@Nonnull List<?> events);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: MateuszNaKodach
body: Added, also for commands
created_at: 2025-04-04T14:10:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,16 +369,12 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result of command message which have been published during the "when" phase.
              *
-             * @param matcher The matcher to verify the actual result message against.
+             * @param consumer Consumes the command result. You may place your own assertions here.
              * @return The current Then instance, for fluent interfacing.
              */
-            Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher);
+            Command resultMessage(@Nonnull Consumer<? super CommandResultMessage<?>> consumer);
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: MateuszNaKodach
body: I've renamed and keep argumnet types. Please check one more time. 
created_at: 2025-04-04T14:19:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +36,24 @@ public class AxonAssertionError extends AssertionError {
      *
      * @param detailMessage a detailed description of the failed assertion
      */
-    public AxonAssertionError(String detailMessage) {
+    public AxonAssertionError(@Nonnull String detailMessage) {
         super(detailMessage);
         StackTraceElement[] original = getStackTrace();
         setStackTrace(cleanStackTrace(original));
     }
 
+    /**
path: test/src/main/java/org/axonframework/test/AxonAssertionError.java
user: login: smcvb
body: Feel free to align this class with our current idea of styling and JavaDoc! I am pretty sure this class hasn't been touched for ages, but as soon as we do touch it, I would like us to clean it up.
created_at: 2025-04-07T08:27:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +36,24 @@ public class AxonAssertionError extends AssertionError {
      *
      * @param detailMessage a detailed description of the failed assertion
      */
-    public AxonAssertionError(String detailMessage) {
+    public AxonAssertionError(@Nonnull String detailMessage) {
         super(detailMessage);
         StackTraceElement[] original = getStackTrace();
         setStackTrace(cleanStackTrace(original));
     }
 
+    /**
+     * Create a new error instance using the given {@code cause} and {@code detailMessage}.
+     *
+     * @param cause         the cause of the error
+     * @param detailMessage a detailed description of the failed assertion
path: test/src/main/java/org/axonframework/test/AxonAssertionError.java
user: login: smcvb
body: So, do things like this, for example:
```suggestion
     * @param cause         The cause of the error.
     * @param detailMessage A detailed description of the failed assertion.
```
created_at: 2025-04-07T08:27:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +36,24 @@ public class AxonAssertionError extends AssertionError {
      *
      * @param detailMessage a detailed description of the failed assertion
      */
-    public AxonAssertionError(String detailMessage) {
+    public AxonAssertionError(@Nonnull String detailMessage) {
         super(detailMessage);
         StackTraceElement[] original = getStackTrace();
         setStackTrace(cleanStackTrace(original));
     }
 
+    /**
+     * Create a new error instance using the given {@code cause} and {@code detailMessage}.
+     *
+     * @param cause         the cause of the error
+     * @param detailMessage a detailed description of the failed assertion
+     */
+    public AxonAssertionError(@Nonnull String detailMessage, @Nonnull Throwable cause) {
+        super(detailMessage, cause);
path: test/src/main/java/org/axonframework/test/AxonAssertionError.java
user: login: smcvb
body: And I guess we can do:
```suggestion
        super(Objects.requireNonNull(detailMessage), Objects.requireNonNull(cause));
```
created_at: 2025-04-07T08:28:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +36,24 @@ public class AxonAssertionError extends AssertionError {
      *
      * @param detailMessage a detailed description of the failed assertion
      */
-    public AxonAssertionError(String detailMessage) {
+    public AxonAssertionError(@Nonnull String detailMessage) {
         super(detailMessage);
         StackTraceElement[] original = getStackTrace();
         setStackTrace(cleanStackTrace(original));
     }
 
+    /**
path: test/src/main/java/org/axonframework/test/AxonAssertionError.java
user: login: smcvb
body: This change should've updated the copyright notice of the file too, by the way.
created_at: 2025-04-07T08:29:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -361,16 +392,12 @@ interface Command extends Message<Command> {
             Command success();
 
             /**
-             * Expect the last command handler from the When phase to return a result message that matches the given
-             * {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result of command message which have been published during the "when" phase.
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: smcvb
body: I would reword this slightly:
```suggestion
             * Invokes the given {@code consumer} of the command result message that has been returned during the "when" phase, allowing for <b>any</b> form of assertion.
```
created_at: 2025-04-07T09:52:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -385,39 +412,13 @@ interface Command extends Message<Command> {
             Command resultMessagePayload(@Nonnull Object expectedPayload);
 
             /**
-             * Expect the last command handler from the When phase to return a payload that matches the given
-             * {@code matcher} after execution.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
-             *
-             * @param matcher The matcher to verify the actual return value against.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            Command resultMessagePayloadMatching(@Nonnull Matcher<?> matcher);
-
-            /**
-             * Expect the given {@code expectedException} to occur during the When phase execution. The actual exception
-             * should be exactly of that type, subclasses are not accepted.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
-             *
-             * @param expectedException The type of exception expected from the When phase execution.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            Command exception(@Nonnull Class<? extends Throwable> expectedException);
-
-            /**
-             * Expect an exception to occur during the When phase that matches with the given {@code matcher}.
-             * <p>
-             * Only take commands into account that were dispatched explicitly with the {@link When#command}. Hence, do
-             * not take into accounts commands dispatched as side effects of the message handlers.
+             * Allow to consume the result payload of command message which have been published during the "when"
+             * phase.
              *
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: smcvb
body: I would reword this slightly:
```suggestion
             * Invokes the given {@code consumer} of the command result payload that has been returned during the "when" phase, allowing for <b>any</b> form of assertion.
```
created_at: 2025-04-07T09:54:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -495,23 +471,13 @@ interface Message<T extends Message<T>> {
              */
             T events(@Nonnull EventMessage<?>... expectedEvents);
 
-            /**
-             * Expect the published events during the {@link When} phase to match the given {@code matcher}.
-             * <p>
-             * Note: if no events were published, the matcher receives an empty List.
-             *
-             * @param matcher The matcher to match with the actually published events.
-             * @return The current Then instance, for fluent interfacing.
-             */
-            T events(@Nonnull Matcher<? extends List<? super EventMessage<?>>> matcher);
-
             /**
              * Allow to consume the set of event messages which have been published during the "when" phase.
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: smcvb
body: I would reword this slightly:
```suggestion
             * Invokes the given {@code consumer} of the set of event messages that have been published during the "when" phase, allowing for <b>any</b> form of assertion.
```
created_at: 2025-04-07T09:55:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -562,7 +528,7 @@ default T noEvents() {
              * @param consumer Consumes the dispatched commands. You may place your own assertions here.
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: smcvb
body: I would reword the JavaDoc body here similarly as the previous recommendations I've made.
created_at: 2025-04-07T09:56:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -607,6 +573,42 @@ default T noEvents() {
              *
              * @return a {@link Setup} instance that allows configuring a new test scenario.
              */
+
+            /**
+             * Expect the given {@code expectedException} to occur during the When phase execution. The actual exception
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: smcvb
body: Super duper nit: do we refer to the when-phase as `When` or `"when"`? I've seen both being used now, and think it's better if we take a single approach.
created_at: 2025-04-07T09:57:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -607,6 +573,42 @@ default T noEvents() {
              *
              * @return a {@link Setup} instance that allows configuring a new test scenario.
              */
+
+            /**
+             * Expect the given {@code expectedException} to occur during the When phase execution. The actual exception
+             * should be exactly of that type, subclasses are not accepted.
+             * <p>
+             * Only take messages into account that were published explicitly with the {@link When#command} or
+             * {@link When#event}. Hence, do not take into accounts messages published as side effects of other message
+             * handlers present in the configuration.
+             *
+             * @param type The type of exception expected from the When phase execution.
+             * @return The current Then instance, for fluent interfacing.
+             */
+            T exception(@Nonnull Class<? extends Throwable> type);
+
+            /**
+             * Expect the exception with given {@code type} and {@code message} to occur during the When phase
+             * execution. The actual exception should be exactly of that type, subclasses are not accepted.
+             * <p>
+             * Only take messages into account that were published explicitly with the {@link When#command} or
+             * {@link When#event}. Hence, do not take into accounts messages published as side effects of other message
+             * handlers present in the configuration.
+             *
+             * @param type    The type of exception expected from the When phase execution.
+             * @param message The message of the exception expected from the When phase execution.
+             * @return The current Then instance, for fluent interfacing.
+             */
+            T exception(@Nonnull Class<? extends Throwable> type, @Nonnull String message);
+
+            /**
+             * Allow to consume the exception which have been thrown during the "when" phase.
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: smcvb
body: I would reword this slightly:
```suggestion
             * Invokes the given {@code consumer} of the exception that has been throw during the "when" phase, allowing for <b>any</b> form of assertion.
```
created_at: 2025-04-07T09:58:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,20 +49,21 @@ public AxonTestThenCommand(
 
     @Override
     public AxonTestPhase.Then.Command success() {
-        return resultMessage(anything());
+        return resultMessageSatisfies(c -> {
+        });
     }
 
     @Override
-    public AxonTestPhase.Then.Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher) {
-        if (matcher == null) {
-            return resultMessage(nullValue());
-        }
+    public AxonTestPhase.Then.Command resultMessageSatisfies(
+            @Nonnull Consumer<? super CommandResultMessage<?>> consumer) {
         StringDescription expectedDescription = new StringDescription();
-        matcher.describeTo(expectedDescription);
         if (actualException != null) {
             reporter.reportUnexpectedException(actualException, expectedDescription);
-        } else if (!matcher.matches(actualResult)) {
-            reporter.reportWrongResult(actualResult, expectedDescription);
+        }
+        try {
+            consumer.accept((CommandResultMessage<?>) actualResult);
+        } catch (AssertionError e) {
+            reporter.reportWrongResult(actualResult, "Result message to satisfy custom assertions: " + e.getMessage());
path: test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java
user: login: smcvb
body: Although I cannot foresee the entire block of text we pump out right now, wouldn't the following sentence make slightly more sense? Not sure to be honest...perhaps you can share a sample of would the assertion error looks like in the pull request's description?

```suggestion
            reporter.reportWrongResult(actualResult, "Result message should satisfy custom assertion(s): " + e.getMessage());
```
created_at: 2025-04-07T10:05:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,12 +36,24 @@ public class AxonAssertionError extends AssertionError {
      *
      * @param detailMessage a detailed description of the failed assertion
      */
-    public AxonAssertionError(String detailMessage) {
+    public AxonAssertionError(@Nonnull String detailMessage) {
         super(detailMessage);
         StackTraceElement[] original = getStackTrace();
         setStackTrace(cleanStackTrace(original));
     }
 
+    /**
path: test/src/main/java/org/axonframework/test/AxonAssertionError.java
user: login: MateuszNaKodach
body: done 
created_at: 2025-04-07T12:45:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -607,6 +573,42 @@ default T noEvents() {
              *
              * @return a {@link Setup} instance that allows configuring a new test scenario.
              */
+
+            /**
+             * Expect the given {@code expectedException} to occur during the When phase execution. The actual exception
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: MateuszNaKodach
body: Thanks that you've spotted that! I've changed everything to Given When Then, without quotes. 
created_at: 2025-04-07T13:01:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,20 +49,21 @@ public AxonTestThenCommand(
 
     @Override
     public AxonTestPhase.Then.Command success() {
-        return resultMessage(anything());
+        return resultMessageSatisfies(c -> {
+        });
     }
 
     @Override
-    public AxonTestPhase.Then.Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher) {
-        if (matcher == null) {
-            return resultMessage(nullValue());
-        }
+    public AxonTestPhase.Then.Command resultMessageSatisfies(
+            @Nonnull Consumer<? super CommandResultMessage<?>> consumer) {
         StringDescription expectedDescription = new StringDescription();
-        matcher.describeTo(expectedDescription);
         if (actualException != null) {
             reporter.reportUnexpectedException(actualException, expectedDescription);
-        } else if (!matcher.matches(actualResult)) {
-            reporter.reportWrongResult(actualResult, expectedDescription);
+        }
+        try {
+            consumer.accept((CommandResultMessage<?>) actualResult);
+        } catch (AssertionError e) {
+            reporter.reportWrongResult(actualResult, "Result message to satisfy custom assertions: " + e.getMessage());
path: test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java
user: login: MateuszNaKodach
body: ```
Expected <Result message to satisfy custom assertions: expected: not <null>>,
 but got <null>.
 ````
 This is the full message. Which one is better? Or do you have another idea? 
 I think I we will be rewriting the Reporter for future milestones to make it event better. I have a dream :D... to do it very verbose and showing result of every events, handlers, correlation/causation - to make debugging very easy. 
created_at: 2025-04-07T13:04:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,20 +49,21 @@ public AxonTestThenCommand(
 
     @Override
     public AxonTestPhase.Then.Command success() {
-        return resultMessage(anything());
+        return resultMessageSatisfies(c -> {
+        });
     }
 
     @Override
-    public AxonTestPhase.Then.Command resultMessage(@Nonnull Matcher<? super CommandResultMessage<?>> matcher) {
-        if (matcher == null) {
-            return resultMessage(nullValue());
-        }
+    public AxonTestPhase.Then.Command resultMessageSatisfies(
+            @Nonnull Consumer<? super CommandResultMessage<?>> consumer) {
         StringDescription expectedDescription = new StringDescription();
-        matcher.describeTo(expectedDescription);
         if (actualException != null) {
             reporter.reportUnexpectedException(actualException, expectedDescription);
-        } else if (!matcher.matches(actualResult)) {
-            reporter.reportWrongResult(actualResult, expectedDescription);
+        }
+        try {
+            consumer.accept((CommandResultMessage<?>) actualResult);
+        } catch (AssertionError e) {
+            reporter.reportWrongResult(actualResult, "Result message to satisfy custom assertions: " + e.getMessage());
path: test/src/main/java/org/axonframework/test/fixture/AxonTestThenCommand.java
user: login: MateuszNaKodach
body: And another example:
```
org.axonframework.test.AxonAssertionError: The command handler returned an unexpected value

Expected <Result message to satisfy custom assertions: 
<Click to see difference>


	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
```

![image](https://github.com/user-attachments/assets/f15d58f6-dccb-4e3b-99f4-77c0aa65137a)

created_at: 2025-04-07T13:10:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,7 +113,7 @@ public Customization() {
         /**
          * Registers the given {@code fieldFilter}, which is used to define which Fields are used when comparing
          * objects. The {@link org.axonframework.test.fixture.AxonTestPhase.Then.Event#events} and
-         * {@link org.axonframework.test.fixture.AxonTestPhase.Then.Command#resultMessage}, for example, use this
+         * {@link org.axonframework.test.fixture.AxonTestPhase.Then.Command#resultMessageSatisfies}, for example, use this
path: test/src/main/java/org/axonframework/test/fixture/AxonTestFixture.java
user: login: CodeDrivenMitch
body: This is not true right? The filter is not used by the `satifies`, but by the `matches`
created_at: 2025-04-08T09:13:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -231,16 +242,36 @@ default Given command(@Nonnull Object payload, @Nonnull Map<String, ?> metaData)
         Given commands(@Nonnull CommandMessage<?>... messages);
 
         /**
-         * Transitions to the "when" phase to execute the test action.
+         * Configures the given {@code commands} as commands in the "given" state. These commands will be dispatched in
+         * the order they are provided in the same Unit of Work.
+         *
+         * @param commands The command messages to dispatch.
+         * @return The current Given instance, for fluent interfacing.
+         */
+        default Given commands(@Nonnull Object... commands) {
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: CodeDrivenMitch
body: I think the commands should each get their own UoW, or they won't be able to see each other's events right?
Usually this is part of a flow
created_at: 2025-04-08T09:15:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -231,16 +242,36 @@ default Given command(@Nonnull Object payload, @Nonnull Map<String, ?> metaData)
         Given commands(@Nonnull CommandMessage<?>... messages);
 
         /**
-         * Transitions to the "when" phase to execute the test action.
+         * Configures the given {@code commands} as commands in the "given" state. These commands will be dispatched in
+         * the order they are provided in the same Unit of Work.
+         *
+         * @param commands The command messages to dispatch.
+         * @return The current Given instance, for fluent interfacing.
+         */
+        default Given commands(@Nonnull Object... commands) {
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: CodeDrivenMitch
body: For events it makes sense to have multiple in one UoW, but for commands not.
created_at: 2025-04-08T09:16:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -231,16 +242,36 @@ default Given command(@Nonnull Object payload, @Nonnull Map<String, ?> metaData)
         Given commands(@Nonnull CommandMessage<?>... messages);
 
         /**
-         * Transitions to the "when" phase to execute the test action.
+         * Configures the given {@code commands} as commands in the "given" state. These commands will be dispatched in
+         * the order they are provided in the same Unit of Work.
+         *
+         * @param commands The command messages to dispatch.
+         * @return The current Given instance, for fluent interfacing.
+         */
+        default Given commands(@Nonnull Object... commands) {
path: test/src/main/java/org/axonframework/test/fixture/AxonTestPhase.java
user: login: MateuszNaKodach
body: Hmmm... I'm not sure about that. Your assumption is safer.

But... what if you'd like to execute multiple commands transactionally? So in the same unit of work. It was possible in AF4 to execute two commands using same Unit Of Work, if SimpleCommandBus used. 
created_at: 2025-04-08T10:05:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -113,7 +113,7 @@ public Customization() {
         /**
          * Registers the given {@code fieldFilter}, which is used to define which Fields are used when comparing
          * objects. The {@link org.axonframework.test.fixture.AxonTestPhase.Then.Event#events} and
-         * {@link org.axonframework.test.fixture.AxonTestPhase.Then.Command#resultMessage}, for example, use this
+         * {@link org.axonframework.test.fixture.AxonTestPhase.Then.Command#resultMessageSatisfies}, for example, use this
path: test/src/main/java/org/axonframework/test/fixture/AxonTestFixture.java
user: login: MateuszNaKodach
body: Docs fixed, I added a list of methods where it applies :) 
created_at: 2025-04-08T12:42:44Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-08T13:18:35Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=AxonFramework_AxonFramework&pullRequest=3366) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [58.3% Coverage on New Code](https://sonarcloud.io/component_measures?id=AxonFramework_AxonFramework&pullRequest=3366&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=AxonFramework_AxonFramework&pullRequest=3366)


author_association: NONE