All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.axonframework.test.aggregate.AggregateTestFixture Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2010-2024. Axon Framework
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.axonframework.test.aggregate;

import org.axonframework.commandhandling.AnnotationCommandHandlerAdapter;
import org.axonframework.commandhandling.CommandBus;
import org.axonframework.commandhandling.CommandCallback;
import org.axonframework.commandhandling.CommandMessage;
import org.axonframework.commandhandling.CommandResultMessage;
import org.axonframework.commandhandling.GenericCommandMessage;
import org.axonframework.commandhandling.SimpleCommandBus;
import org.axonframework.common.Assert;
import org.axonframework.common.Registration;
import org.axonframework.deadline.DeadlineMessage;
import org.axonframework.eventhandling.DomainEventMessage;
import org.axonframework.eventhandling.EventBus;
import org.axonframework.eventhandling.EventMessage;
import org.axonframework.eventhandling.GenericDomainEventMessage;
import org.axonframework.eventhandling.GenericEventMessage;
import org.axonframework.eventhandling.TrackingEventStream;
import org.axonframework.eventhandling.TrackingToken;
import org.axonframework.eventsourcing.AggregateFactory;
import org.axonframework.eventsourcing.EventSourcedAggregate;
import org.axonframework.eventsourcing.EventSourcingRepository;
import org.axonframework.eventsourcing.GenericAggregateFactory;
import org.axonframework.eventsourcing.eventstore.DomainEventStream;
import org.axonframework.eventsourcing.eventstore.EventStore;
import org.axonframework.eventsourcing.eventstore.EventStoreException;
import org.axonframework.messaging.GenericMessage;
import org.axonframework.messaging.Message;
import org.axonframework.messaging.MessageDispatchInterceptor;
import org.axonframework.messaging.MessageHandler;
import org.axonframework.messaging.MessageHandlerInterceptor;
import org.axonframework.messaging.MetaData;
import org.axonframework.messaging.ScopeDescriptor;
import org.axonframework.messaging.annotation.ClasspathHandlerDefinition;
import org.axonframework.messaging.annotation.ClasspathHandlerEnhancerDefinition;
import org.axonframework.messaging.annotation.ClasspathParameterResolverFactory;
import org.axonframework.messaging.annotation.HandlerDefinition;
import org.axonframework.messaging.annotation.HandlerEnhancerDefinition;
import org.axonframework.messaging.annotation.MultiHandlerDefinition;
import org.axonframework.messaging.annotation.MultiHandlerEnhancerDefinition;
import org.axonframework.messaging.annotation.MultiParameterResolverFactory;
import org.axonframework.messaging.annotation.ParameterResolverFactory;
import org.axonframework.messaging.annotation.SimpleResourceParameterResolverFactory;
import org.axonframework.messaging.unitofwork.CurrentUnitOfWork;
import org.axonframework.messaging.unitofwork.DefaultUnitOfWork;
import org.axonframework.messaging.unitofwork.UnitOfWork;
import org.axonframework.modelling.command.Aggregate;
import org.axonframework.modelling.command.AggregateAnnotationCommandHandler;
import org.axonframework.modelling.command.AggregateNotFoundException;
import org.axonframework.modelling.command.AggregateScopeDescriptor;
import org.axonframework.modelling.command.CommandTargetResolver;
import org.axonframework.modelling.command.ConflictingAggregateVersionException;
import org.axonframework.modelling.command.Repository;
import org.axonframework.modelling.command.RepositoryProvider;
import org.axonframework.modelling.command.inspection.AggregateModel;
import org.axonframework.modelling.command.inspection.AnnotatedAggregate;
import org.axonframework.modelling.command.inspection.AnnotatedAggregateMetaModelFactory;
import org.axonframework.test.AxonAssertionError;
import org.axonframework.test.FixtureExecutionException;
import org.axonframework.test.deadline.StubDeadlineManager;
import org.axonframework.test.matchers.FieldFilter;
import org.axonframework.test.matchers.IgnoreField;
import org.axonframework.test.matchers.MatchAllFieldFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.annotation.Nonnull;

import static java.lang.String.format;
import static org.axonframework.common.ReflectionUtils.*;

/**
 * A test fixture that allows the execution of given-when-then style test cases. For detailed usage information, see
 * {@link FixtureConfiguration}.
 *
 * @param  The type of Aggregate tested in this Fixture
 * @author Allard Buijze
 * @since 0.6
 */
public class AggregateTestFixture implements FixtureConfiguration, TestExecutor {

    private static final Logger logger = LoggerFactory.getLogger(AggregateTestFixture.class);

    private final Class aggregateType;
    private final Set> subtypes = new HashSet<>();
    private final SimpleCommandBus commandBus;
    private final EventStore eventStore;
    private final List fieldFilters = new ArrayList<>();
    private final List resources = new ArrayList<>();
    private boolean useStateStorage;
    private RepositoryProvider repositoryProvider;
    private IdentifierValidatingRepository repository;
    private final StubDeadlineManager deadlineManager;
    private String aggregateIdentifier;
    private Deque> givenEvents;
    private Deque> storedEvents;
    private List> publishedEvents;
    private long sequenceNumber;
    private boolean reportIllegalStateChange = true;
    private boolean explicitCommandHandlersSet;
    private final LinkedList registeredParameterResolverFactories = new LinkedList<>();
    private final LinkedList registeredHandlerDefinitions = new LinkedList<>();
    private final LinkedList registeredHandlerEnhancerDefinitions = new LinkedList<>();
    private CommandTargetResolver commandTargetResolver;

    /**
     * Initializes a new given-when-then style test fixture for the given {@code aggregateType}.
     *
     * @param aggregateType the aggregate to initialize the test fixture for
     */
    public AggregateTestFixture(Class aggregateType) {
        deadlineManager = new StubDeadlineManager();
        commandBus = SimpleCommandBus.builder().build();
        eventStore = new RecordingEventStore();
        resources.add(commandBus);
        resources.add(eventStore);
        resources.add(deadlineManager);

        this.aggregateType = aggregateType;
        this.storedEvents = new LinkedList<>();
        this.publishedEvents = new ArrayList<>();
        this.givenEvents = new LinkedList<>();
        this.sequenceNumber = 0;

        registeredParameterResolverFactories.add(new SimpleResourceParameterResolverFactory(resources));
        registeredParameterResolverFactories.add(ClasspathParameterResolverFactory.forClass(aggregateType));
        registeredHandlerDefinitions.add(ClasspathHandlerDefinition.forClass(aggregateType));
        registeredHandlerEnhancerDefinitions.add(ClasspathHandlerEnhancerDefinition.forClass(aggregateType));
    }

    @SafeVarargs
    @Override
    public final FixtureConfiguration withSubtypes(Class... subtypes) {
        this.subtypes.addAll(Arrays.asList(subtypes));
        return this;
    }

    @Override
    public FixtureConfiguration useStateStorage() {
        this.useStateStorage = true;
        return this;
    }

    @Override
    public FixtureConfiguration registerRepository(Repository repository) {
        this.repository = new IdentifierValidatingRepository<>(repository);
        resources.add(repository);
        return this;
    }

    @Override
    public FixtureConfiguration registerRepositoryProvider(RepositoryProvider repositoryProvider) {
        if (repository != null) {
            throw new FixtureExecutionException(
                    "Cannot register a RepositoryProvider since the Repository is already defined in this fixture."
                            + " It is recommended to first a RepositoryProvider"
                            + " and then register or retrieve the Repository."
            );
        }
        this.repositoryProvider = repositoryProvider;
        return this;
    }

    @Override
    public FixtureConfiguration registerAggregateFactory(AggregateFactory aggregateFactory) {
        return registerRepository(EventSourcingRepository.builder(aggregateFactory.getAggregateType())
                                                         .aggregateFactory(aggregateFactory)
                                                         .eventStore(eventStore)
                                                         .parameterResolverFactory(getParameterResolverFactory())
                                                         .handlerDefinition(getHandlerDefinition())
                                                         .repositoryProvider(getRepositoryProvider())
                                                         .build());
    }

    @Override
    public synchronized FixtureConfiguration registerAnnotatedCommandHandler(final Object annotatedCommandHandler) {
        registerAggregateCommandHandlers();
        explicitCommandHandlersSet = true;
        AnnotationCommandHandlerAdapter adapter = new AnnotationCommandHandlerAdapter<>(
                annotatedCommandHandler, getParameterResolverFactory(), getHandlerDefinition()
        );
        //noinspection resource
        adapter.subscribe(commandBus);
        return this;
    }

    @Override
    public FixtureConfiguration registerCommandHandler(Class payloadType,
                                                          MessageHandler> commandHandler) {
        return registerCommandHandler(payloadType.getName(), commandHandler);
    }

    @Override
    public FixtureConfiguration registerCommandHandler(String commandName,
                                                          MessageHandler> commandHandler) {
        registerAggregateCommandHandlers();
        explicitCommandHandlersSet = true;
        //noinspection resource
        commandBus.subscribe(commandName, commandHandler);
        return this;
    }


    @Override
    public FixtureConfiguration registerInjectableResource(Object resource) {
        if (explicitCommandHandlersSet) {
            throw new FixtureExecutionException("Cannot inject resources after command handler has been created. " +
                                                        "Configure all resource before calling " +
                                                        "registerCommandHandler() or " +
                                                        "registerAnnotatedCommandHandler()");
        }
        this.resources.add(resource);
        return this;
    }

    @Override
    public FixtureConfiguration registerParameterResolverFactory(ParameterResolverFactory parameterResolverFactory) {
        if (repository != null) {
            throw new FixtureExecutionException(
                    "Cannot register more ParameterResolverFactories since the Repository is already defined"
                            + " in this fixture. It is recommended to first register ParameterResolverFactories"
                            + " and then register or retrieve the Repository."
            );
        }
        this.registeredParameterResolverFactories.addFirst(parameterResolverFactory);
        return this;
    }

    @Override
    public FixtureConfiguration registerCommandDispatchInterceptor(
            MessageDispatchInterceptor> commandDispatchInterceptor
    ) {
        //noinspection resource
        this.commandBus.registerDispatchInterceptor(commandDispatchInterceptor);
        return this;
    }

    @Override
    public FixtureConfiguration registerCommandHandlerInterceptor(
            MessageHandlerInterceptor> commandHandlerInterceptor
    ) {
        //noinspection resource
        this.commandBus.registerHandlerInterceptor(commandHandlerInterceptor);
        return this;
    }

    @Override
    public FixtureConfiguration registerDeadlineDispatchInterceptor(
            MessageDispatchInterceptor> deadlineDispatchInterceptor) {
        //noinspection resource
        this.deadlineManager.registerDispatchInterceptor(deadlineDispatchInterceptor);
        return this;
    }

    @Override
    public FixtureConfiguration registerDeadlineHandlerInterceptor(
            MessageHandlerInterceptor> deadlineHandlerInterceptor) {
        //noinspection resource
        this.deadlineManager.registerHandlerInterceptor(deadlineHandlerInterceptor);
        return this;
    }

    @Override
    public FixtureConfiguration registerFieldFilter(FieldFilter fieldFilter) {
        this.fieldFilters.add(fieldFilter);
        return this;
    }

    @Override
    public FixtureConfiguration registerIgnoredField(Class declaringClass, String fieldName) {
        return registerFieldFilter(new IgnoreField(declaringClass, fieldName));
    }

    @Override
    public FixtureConfiguration registerHandlerDefinition(HandlerDefinition handlerDefinition) {
        if (repository != null) {
            throw new FixtureExecutionException(
                    "Cannot register more HandlerDefinitions since the Repository is already defined in this fixture."
                            + " It is recommended to first register HandlerDefinitions"
                            + " and then register or retrieve the Repository."
            );
        }
        this.registeredHandlerDefinitions.addFirst(handlerDefinition);
        return this;
    }

    @Override
    public FixtureConfiguration registerHandlerEnhancerDefinition(
            HandlerEnhancerDefinition handlerEnhancerDefinition
    ) {
        if (repository != null) {
            throw new FixtureExecutionException(
                    "Cannot register more HandlerEnhancerDefinitions since the Repository is already defined"
                            + " in this fixture. It is recommended to first register HandlerEnhancerDefinitions"
                            + " and then register or retrieve the Repository."
            );
        }
        this.registeredHandlerEnhancerDefinitions.addFirst(handlerEnhancerDefinition);
        return this;
    }

    @Override
    public FixtureConfiguration registerCommandTargetResolver(CommandTargetResolver commandTargetResolver) {
        this.commandTargetResolver = commandTargetResolver;
        return this;
    }

    @Override
    public TestExecutor given(Object... domainEvents) {
        return given(Arrays.asList(domainEvents));
    }

    @Override
    public TestExecutor andGiven(Object... domainEvents) {
        return andGiven(Arrays.asList(domainEvents));
    }

    @Override
    public TestExecutor givenNoPriorActivity() {
        ensureRepositoryConfiguration();
        clearGivenWhenState();
        return this;
    }

    @Override
    public TestExecutor givenState(Supplier aggregate) {
        if (this.repository == null) {
            this.useStateStorage();
        }

        ensureRepositoryConfiguration();
        DefaultUnitOfWork.startAndGet(null).execute(() -> {
            try {
                repository.newInstance(aggregate::get);
            } catch (Exception e) {
                throw new FixtureExecutionException(
                        "An exception occurred while trying to initialize repository with given aggregate (using 'givenState')",
                        e);
            }
        });
        clearGivenWhenState();
        return this;
    }

    @Override
    public TestExecutor given(List domainEvents) {
        ensureRepositoryConfiguration();
        clearGivenWhenState();
        return andGiven(domainEvents);
    }

    @Override
    public TestExecutor andGiven(List domainEvents) {
        if (this.useStateStorage) {
            throw new FixtureExecutionException(
                    "Given events not supported, because the fixture is configured to use state storage");
        }

        for (Object event : domainEvents) {
            Object payload = event;
            MetaData metaData = null;
            String type = aggregateType.getSimpleName();
            if (event instanceof Message) {
                payload = ((Message) event).getPayload();
                metaData = ((Message) event).getMetaData();
            }
            if (event instanceof DomainEventMessage) {
                type = ((DomainEventMessage) event).getType();
            }
            GenericDomainEventMessage eventMessage = new GenericDomainEventMessage<>(
                    type,
                    aggregateIdentifier,
                    sequenceNumber++,
                    new GenericMessage<>(payload, metaData),
                    deadlineManager.getCurrentDateTime()
            );
            this.givenEvents.add(eventMessage);
        }
        return this;
    }

    @Override
    public TestExecutor givenCommands(Object... commands) {
        return givenCommands(Arrays.asList(commands));
    }

    @Override
    public TestExecutor andGivenCommands(Object... commands) {
        return andGivenCommands(Arrays.asList(commands));
    }

    @Override
    public TestExecutor givenCommands(List commands) {
        clearGivenWhenState();
        return andGivenCommands(commands);
    }

    @Override
    public TestExecutor andGivenCommands(List commands) {
        finalizeConfiguration();
        for (Object command : commands) {
            ExecutionExceptionAwareCallback callback = new ExecutionExceptionAwareCallback();
            CommandMessage commandMessage = GenericCommandMessage.asCommandMessage(command);
            executeAtSimulatedTime(() -> commandBus.dispatch(commandMessage, callback));
            callback.assertSuccessful();
            givenEvents.addAll(storedEvents);
            storedEvents.clear();
        }
        publishedEvents.clear();
        return this;
    }

    private void executeAtSimulatedTime(Runnable runnable) {
        Clock previousClock = GenericEventMessage.clock;
        try {
            GenericEventMessage.clock = Clock.fixed(currentTime(), ZoneOffset.UTC);
            runnable.run();
        } finally {
            GenericEventMessage.clock = previousClock;
        }
    }

    @Override
    public TestExecutor givenCurrentTime(Instant currentTime) {
        clearGivenWhenState();
        return andGivenCurrentTime(currentTime);
    }

    @Override
    public TestExecutor andGivenCurrentTime(Instant currentTime) {
        deadlineManager.initializeAt(currentTime);
        return this;
    }

    @Override
    public Instant currentTime() {
        return deadlineManager.getCurrentDateTime();
    }

    @Override
    public ResultValidator whenTimeElapses(Duration elapsedTime) {
        logger.debug("Starting WHEN-phase");
        deadlineManager.advanceTimeBy(elapsedTime, this::handleDeadline);
        return buildResultValidator();
    }

    @Override
    @Deprecated
    public ResultValidator whenThenTimeElapses(Duration elapsedTime) {
        return whenTimeElapses(elapsedTime);
    }

    @Override
    public ResultValidator whenTimeAdvancesTo(Instant newPointInTime) {
        logger.debug("Starting WHEN-phase");
        deadlineManager.advanceTimeTo(newPointInTime, this::handleDeadline);
        return buildResultValidator();
    }

    @Override
    @Deprecated
    public ResultValidator whenThenTimeAdvancesTo(Instant newPointInTime) {
        return whenTimeAdvancesTo(newPointInTime);
    }

    @Override
    public ResultValidator when(Object command) {
        return when(command, MetaData.emptyInstance());
    }

    @Override
    public ResultValidator when(Object command, Map metaData) {
        return when(resultValidator -> {
            CommandMessage commandMessage = GenericCommandMessage.asCommandMessage(command)
                                                                         .andMetaData(metaData);
            commandBus.dispatch(commandMessage, resultValidator);
        });
    }

    @Override
    public ResultValidator whenConstructing(Callable aggregateFactory) {
        return when(validator -> DefaultUnitOfWork.startAndGet(null).execute(() -> {
            try {
                repository.newInstance(aggregateFactory);
            } catch (Exception | AssertionError e) {
                // Catching AssertionErrors as the Repository of the Fixture may throw them.
                validator.recordException(e);
            }
        }));
    }

    @Override
    public ResultValidator whenInvoking(String aggregateId, Consumer aggregateSupplier) {
        return when(validator -> DefaultUnitOfWork.startAndGet(null).execute(() -> {
            try {
                repository.load(aggregateId)
                          .execute(aggregateSupplier);
            } catch (Exception | AssertionError e) {
                // Catching AssertionErrors as the Repository of the Fixture may throw them.
                validator.recordException(e);
            }
        }));
    }

    private ResultValidator when(Consumer> whenPhase) {
        logger.debug("Starting WHEN-phase");
        finalizeConfiguration();
        final MatchAllFieldFilter fieldFilter = new MatchAllFieldFilter(fieldFilters);
        ResultValidatorImpl resultValidator = new ResultValidatorImpl<>(publishedEvents,
                                                                           fieldFilter,
                                                                           () -> repository.getAggregate(),
                                                                           deadlineManager);

        executeAtSimulatedTime(() -> whenPhase.accept(resultValidator));

        if (!repository.rolledBack) {
            Aggregate workingAggregate = repository.aggregate;
            detectIllegalStateChanges(fieldFilter, workingAggregate);
        }
        resultValidator.assertValidRecording();
        logger.debug("Starting EXPECT-phase");
        return resultValidator;
    }

    /**
     * Handles the given {@code deadlineMessage} in the aggregate described by the given {@code aggregateDescriptor}.
     * Deadline message is handled in the scope of a {@link UnitOfWork}. If handling the deadline results in an
     * exception, the exception will be wrapped in a {@link FixtureExecutionException}.
     *
     * @param aggregateDescriptor A {@link ScopeDescriptor} describing the aggregate under test
     * @param deadlineMessage     The {@link DeadlineMessage} to be handled
     */
    protected void handleDeadline(ScopeDescriptor aggregateDescriptor, DeadlineMessage deadlineMessage)
            throws Exception {
        ensureRepositoryConfiguration();
        repository.send(deadlineMessage, aggregateDescriptor);
    }

    private ResultValidator buildResultValidator() {
        MatchAllFieldFilter fieldFilter = new MatchAllFieldFilter(fieldFilters);
        ResultValidatorImpl resultValidator = new ResultValidatorImpl<>(publishedEvents,
                                                                           fieldFilter,
                                                                           () -> repository.getAggregate(),
                                                                           deadlineManager);
        resultValidator.assertValidRecording();
        logger.debug("Starting EXPECT-phase");
        return resultValidator;
    }

    private void finalizeConfiguration() {
        registerAggregateCommandHandlers();
        explicitCommandHandlersSet = true;
    }

    private void registerAggregateCommandHandlers() {
        ensureRepositoryConfiguration();
        if (!explicitCommandHandlersSet) {
            AggregateAnnotationCommandHandler.Builder builder = AggregateAnnotationCommandHandler.builder()
                    .aggregateType(aggregateType)
                    .aggregateModel(aggregateModel())
                    .parameterResolverFactory(getParameterResolverFactory())
                    .repository(this.repository);

            if (commandTargetResolver != null) {
                builder.commandTargetResolver(commandTargetResolver);
            }

            AggregateAnnotationCommandHandler handler = builder.build();
            //noinspection resource
            handler.subscribe(commandBus);
        }
    }

    private void ensureRepositoryConfiguration() {
        if (repository != null) {
            return;
        }

        if (this.useStateStorage) {
            this.registerRepository(new InMemoryRepository<>(
                    aggregateType,
                    subtypes,
                    eventStore,
                    getParameterResolverFactory(),
                    getHandlerDefinition(),
                    getRepositoryProvider()));
        } else {
            AggregateModel aggregateModel = aggregateModel();
            this.registerRepository(EventSourcingRepository.builder(aggregateType)
                                                           .aggregateModel(aggregateModel)
                                                           .aggregateFactory(
                                                                   new GenericAggregateFactory<>(aggregateModel)
                                                           )
                                                           .eventStore(eventStore)
                                                           .parameterResolverFactory(getParameterResolverFactory())
                                                           .handlerDefinition(getHandlerDefinition())
                                                           .repositoryProvider(getRepositoryProvider())
                                                           .build());
        }
    }

    private AggregateModel aggregateModel() {
        return AnnotatedAggregateMetaModelFactory.inspectAggregate(aggregateType,
                                                                   getParameterResolverFactory(),
                                                                   getHandlerDefinition(),
                                                                   subtypes);
    }

    private ParameterResolverFactory getParameterResolverFactory() {
        return MultiParameterResolverFactory.ordered(registeredParameterResolverFactories);
    }

    private HandlerDefinition getHandlerDefinition() {
        HandlerEnhancerDefinition handlerEnhancerDefinition =
                MultiHandlerEnhancerDefinition.ordered(registeredHandlerEnhancerDefinitions);
        return MultiHandlerDefinition.ordered(registeredHandlerDefinitions, handlerEnhancerDefinition);
    }

    private RepositoryProvider getRepositoryProvider() {
        if (repositoryProvider == null) {
            registerRepositoryProvider(new DefaultRepositoryProvider());
        }
        return repositoryProvider;
    }

    private void detectIllegalStateChanges(MatchAllFieldFilter fieldFilter, Aggregate workingAggregate) {
        logger.debug("Starting separate Unit of Work for the purpose of checking illegal state changes in Aggregate");
        if (aggregateIdentifier != null && workingAggregate != null && reportIllegalStateChange) {
            UnitOfWork uow = DefaultUnitOfWork.startAndGet(null);
            try {
                Aggregate aggregate2 = repository.delegate.load(aggregateIdentifier);
                if (workingAggregate.isDeleted()) {
                    throw new AxonAssertionError("The working aggregate was considered deleted, " +
                                                         "but the Repository still contains a non-deleted copy of " +
                                                         "the aggregate. Make sure the aggregate explicitly marks " +
                                                         "itself as deleted in an EventHandler.");
                }
                assertValidWorkingAggregateState(aggregate2, fieldFilter, workingAggregate);
            } catch (AggregateNotFoundException notFound) {
                // The identifier == null if an aggregate creating command handler decided not to create the aggregate.
                if (!workingAggregate.isDeleted() && workingAggregate.identifier() != null) {
                    throw new AxonAssertionError("The working aggregate was not considered deleted, " //NOSONAR
                                                         + "but the Repository cannot recover the state of the " +
                                                         "aggregate, as it is considered deleted there.");
                }
            } catch (Exception e) {
                throw new FixtureExecutionException("An Exception occurred while reconstructing the Aggregate from " +
                                                            "given and published events. This may be an indication " +
                                                            "that the aggregate cannot be recreated from its events.",
                                                    e);
            } finally {
                // rollback to prevent changes bing pushed to event store
                uow.rollback();
            }
        }
    }

    private void assertValidWorkingAggregateState(Aggregate eventSourcedAggregate, MatchAllFieldFilter fieldFilter,
                                                  Aggregate workingAggregate) {
        HashSet comparedEntries = new HashSet<>();
        if (!workingAggregate.rootType().equals(eventSourcedAggregate.rootType())) {
            throw new AxonAssertionError(String.format("The aggregate loaded based on the generated events seems to " +
                                                               "be of another type than the original.\n" +
                                                               "Working type: <%s>\nEvent Sourced type: <%s>",
                                                       workingAggregate.rootType().getName(),
                                                       eventSourcedAggregate.rootType().getName()));
        }
        ensureValuesEqual(workingAggregate.invoke(Function.identity()),
                          eventSourcedAggregate.invoke(Function.identity()),
                          eventSourcedAggregate.rootType().getName(),
                          comparedEntries,
                          fieldFilter);
    }

    private void ensureValuesEqual(Object workingValue,
                                   Object eventSourcedValue,
                                   String propertyPath,
                                   Set comparedEntries,
                                   FieldFilter fieldFilter) {
        if (Objects.equals(workingValue, eventSourcedValue)) {
            // they're equal, nothing more to check...
            return;
        }

        if ((workingValue == null || hasEqualsMethod(workingValue.getClass()))
                || (eventSourcedValue == null || hasEqualsMethod(eventSourcedValue.getClass()))) {
            failIllegalStateChange(workingValue, eventSourcedValue, propertyPath);
        } else if (comparedEntries.add(new ComparationEntry(workingValue, eventSourcedValue))
                && !hasEqualsMethod(workingValue.getClass())) {
            try {
                for (Field field : fieldsOf(workingValue.getClass())) {
                    if (fieldFilter.accept(field)
                            && !Modifier.isStatic(field.getModifiers())
                            && !Modifier.isTransient(field.getModifiers())) {
                        ensureAccessible(field);
                        String newPropertyPath = propertyPath + "." + field.getName();

                        Object workingFieldValue = getFieldValue(field, workingValue);
                        Object eventSourcedFieldValue = getFieldValue(field, eventSourcedValue);
                        ensureValuesEqual(workingFieldValue,
                                          eventSourcedFieldValue,
                                          newPropertyPath,
                                          comparedEntries,
                                          fieldFilter);
                    }
                }
            } catch (Exception e) {
                logger.debug("Exception while attempting to verify deep equality.", e);
                failIllegalStateChange(workingValue, eventSourcedValue, propertyPath);
            }
        }
    }

    private void failIllegalStateChange(Object workingValue, Object eventSourcedValue, String propertyPath) {
        throw new AxonAssertionError(format("Illegal state change detected! " +
                                                    "Property \"%s\" has different value when sourcing events.\n" +
                                                    "Working aggregate value:     <%s>\n" +
                                                    "Value after applying events: <%s>", propertyPath, workingValue,
                                            eventSourcedValue));
    }

    private void clearGivenWhenState() {
        logger.debug("Starting GIVEN-phase");
        storedEvents = new LinkedList<>();
        publishedEvents = new ArrayList<>();
        givenEvents = new LinkedList<>();
        sequenceNumber = 0;
    }

    @Override
    public void setReportIllegalStateChange(boolean reportIllegalStateChange) {
        this.reportIllegalStateChange = reportIllegalStateChange;
    }

    @Override
    public CommandBus getCommandBus() {
        return commandBus;
    }

    @Override
    public EventBus getEventBus() {
        return eventStore;
    }

    @Override
    public EventStore getEventStore() {
        return eventStore;
    }

    @Override
    public Repository getRepository() {
        ensureRepositoryConfiguration();
        return repository;
    }

    private static class ComparationEntry {

        private final Object workingObject;
        private final Object eventSourceObject;

        public ComparationEntry(Object workingObject, Object eventSourceObject) {
            this.workingObject = workingObject;
            this.eventSourceObject = eventSourceObject;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            ComparationEntry that = (ComparationEntry) o;
            return Objects.equals(workingObject, that.workingObject) &&
                    Objects.equals(eventSourceObject, that.eventSourceObject);
        }

        @Override
        public int hashCode() {
            return Objects.hash(workingObject, eventSourceObject);
        }
    }

    private static class IdentifierValidatingRepository implements Repository {

        private final Repository delegate;
        private Aggregate aggregate;
        private boolean rolledBack;

        public IdentifierValidatingRepository(Repository delegate) {
            this.delegate = delegate;
        }

        @Override
        public Aggregate loadOrCreate(@Nonnull String aggregateIdentifier,
                                         @Nonnull Callable factoryMethod) throws Exception {
            CurrentUnitOfWork.get().onRollback(u -> this.rolledBack = true);
            aggregate = delegate.loadOrCreate(aggregateIdentifier, factoryMethod);
            return aggregate;
        }

        @Override
        public Aggregate newInstance(@Nonnull Callable factoryMethod) throws Exception {
            CurrentUnitOfWork.get().onRollback(u -> this.rolledBack = true);
            aggregate = delegate.newInstance(factoryMethod);
            return aggregate;
        }

        @Override
        public Aggregate load(@Nonnull String aggregateIdentifier, Long expectedVersion) {
            CurrentUnitOfWork.get().onRollback(u -> this.rolledBack = true);
            aggregate = delegate.load(aggregateIdentifier, expectedVersion);
            validateIdentifier(aggregateIdentifier, aggregate);
            return aggregate;
        }

        @Override
        public Aggregate load(@Nonnull String aggregateIdentifier) {
            CurrentUnitOfWork.get().onRollback(u -> this.rolledBack = true);
            aggregate = delegate.load(aggregateIdentifier, null);
            validateIdentifier(aggregateIdentifier, aggregate);
            return aggregate;
        }

        private void validateIdentifier(String aggregateIdentifier, Aggregate aggregate) {
            if (aggregateIdentifier != null && !aggregateIdentifier.equals(aggregate.identifierAsString())) {
                throw new AssertionError(String.format(
                        "The aggregate used in this fixture was initialized with an identifier different than " +
                                "the one used to load it. Loaded [%s], but actual identifier is [%s].\n" +
                                "Make sure the identifier passed during construction matches that of the when-phase.",
                        aggregateIdentifier, aggregate.identifierAsString()));
            }
        }

        public Aggregate getAggregate() {
            Assert.state(!rolledBack, () -> "The state of this aggregate cannot be retrieved because it " +
                    "has been modified in a Unit of Work that was rolled back");

            return aggregate;
        }

        @Override
        public void send(Message message, ScopeDescriptor scopeDescription) throws Exception {
            if (canResolve(scopeDescription)) {
                load(((AggregateScopeDescriptor) scopeDescription).getIdentifier().toString()).handle(message);
            }
        }

        @Override
        public boolean canResolve(ScopeDescriptor scopeDescription) {
            return scopeDescription instanceof AggregateScopeDescriptor;
        }
    }

    private static class InMemoryRepository implements Repository {

        private final EventBus eventBus;
        private final RepositoryProvider repositoryProvider;
        private final AggregateModel aggregateModel;
        private AnnotatedAggregate storedAggregate;

        protected InMemoryRepository(Class aggregateType,
                                     Set> subtypes,
                                     EventBus eventBus,
                                     ParameterResolverFactory parameterResolverFactory,
                                     HandlerDefinition handlerDefinition,
                                     RepositoryProvider repositoryProvider) {
            this.aggregateModel = AnnotatedAggregateMetaModelFactory.inspectAggregate(
                    aggregateType, parameterResolverFactory, handlerDefinition, subtypes
            );
            this.eventBus = eventBus;
            this.repositoryProvider = repositoryProvider;
        }

        @Override
        public Aggregate newInstance(@Nonnull Callable factoryMethod) throws Exception {
            Assert.state(storedAggregate == null,
                         () -> "Creating an Aggregate while one is already stored. Test fixtures do not allow multiple instances to be stored.");
            storedAggregate = AnnotatedAggregate.initialize(factoryMethod,
                                                            aggregateModel,
                                                            eventBus,
                                                            repositoryProvider,
                                                            true);
            return storedAggregate;
        }

        @Override
        public Aggregate load(@Nonnull String aggregateIdentifier) {
            return load(aggregateIdentifier, null);
        }

        @Override
        public Aggregate load(@Nonnull String aggregateIdentifier, Long expectedVersion) {
            if (storedAggregate == null) {
                throw new AggregateNotFoundException(aggregateIdentifier,
                                                     "Aggregate not found. No aggregate has been stored yet.");
            }
            if (!aggregateIdentifier.equals(storedAggregate.identifier().toString())) {
                throw new AggregateNotFoundException(
                        aggregateIdentifier,
                        "Aggregate not found. Did you mean to load " + storedAggregate.identifier() + "?"
                );
            }
            if (storedAggregate.isDeleted()) {
                throw new AggregateNotFoundException(aggregateIdentifier, "Aggregate not found. It has been deleted.");
            }
            if (expectedVersion != null && !Objects.equals(expectedVersion, storedAggregate.version())) {
                throw new ConflictingAggregateVersionException(aggregateIdentifier,
                                                               expectedVersion,
                                                               storedAggregate.version());
            }
            return storedAggregate;
        }

        @Override
        public void send(Message message, ScopeDescriptor scopeDescription) throws Exception {
            if (canResolve(scopeDescription)) {
                load(((AggregateScopeDescriptor) scopeDescription).getIdentifier().toString()).handle(message);
            }
        }

        @Override
        public boolean canResolve(ScopeDescriptor scopeDescription) {
            return scopeDescription instanceof AggregateScopeDescriptor;
        }

        @Override
        public Aggregate loadOrCreate(@Nonnull String aggregateIdentifier,
                                         @Nonnull Callable factoryMethod) throws Exception {
            if (storedAggregate == null) {
                return newInstance(factoryMethod);
            }

            return load(aggregateIdentifier);
        }
    }

    private class RecordingEventStore implements EventStore {

        @Override
        public DomainEventStream readEvents(@Nonnull String identifier) {
            if (aggregateIdentifier != null && !aggregateIdentifier.equals(identifier)) {
                String exceptionMessage = format(
                        "The aggregate identifier used in the 'when' step does not resemble the aggregate identifier"
                                + " used in the 'given' step. "
                                + "Please make sure the when-identifier [%s] resembles the given-identifier [%s].",
                        identifier, aggregateIdentifier
                );
                throw new EventStoreException(exceptionMessage);
            } else if (aggregateIdentifier == null) {
                aggregateIdentifier = identifier;
                injectAggregateIdentifier();
            }
            List> allEvents = new ArrayList<>(givenEvents);
            allEvents.addAll(storedEvents);
            if (allEvents.isEmpty()) {
                throw new AggregateNotFoundException(identifier,
                                                     "No 'given' events were configured for this aggregate, " +
                                                             "nor have any events been stored.");
            }
            return DomainEventStream.of(allEvents);
        }

        @Override
        public void publish(@Nonnull List> events) {
            if (CurrentUnitOfWork.isStarted()) {
                CurrentUnitOfWork.get().onPrepareCommit(u -> doAppendEvents(events));
            } else {
                doAppendEvents(events);
            }
        }

        protected void doAppendEvents(List> events) {
            events.forEach(e -> {
                if (!DomainEventMessage.class.isInstance(e)) {
                    // Since the event is not a domain event, only publish it i.o. validating/storing it.
                    publishedEvents.add(e);
                    return;
                }
                DomainEventMessage event = (DomainEventMessage) e;

                if (aggregateIdentifier == null) {
                    aggregateIdentifier = event.getAggregateIdentifier();
                    injectAggregateIdentifier();
                }

                DomainEventMessage lastEvent = (storedEvents.isEmpty() ? givenEvents : storedEvents).peekLast();

                if (lastEvent != null) {
                    if (!lastEvent.getAggregateIdentifier().equals(event.getAggregateIdentifier())) {
                        throw new EventStoreException("Writing events for an unexpected aggregate. This could "
                                                              + "indicate that a wrong aggregate is being triggered.");
                    } else if (lastEvent.getSequenceNumber() != event.getSequenceNumber() - 1) {
                        throw new EventStoreException(format(
                                "Unexpected sequence number on stored event. " + "Expected %s, \n but got %s.",
                                lastEvent.getSequenceNumber() + 1, event.getSequenceNumber()
                        ));
                    }
                }
                publishedEvents.add(event);
                storedEvents.add(event);
            });
        }

        private void injectAggregateIdentifier() {
            List> oldEvents = new ArrayList<>(givenEvents);
            givenEvents.clear();
            for (DomainEventMessage oldEvent : oldEvents) {
                if (oldEvent.getAggregateIdentifier() == null) {
                    givenEvents.add(new GenericDomainEventMessage<>(oldEvent.getType(), aggregateIdentifier,
                                                                    oldEvent.getSequenceNumber(), oldEvent.getPayload(),
                                                                    oldEvent.getMetaData(), oldEvent.getIdentifier(),
                                                                    oldEvent.getTimestamp()));
                } else {
                    givenEvents.add(oldEvent);
                }
            }
        }

        @Override
        public TrackingEventStream openStream(TrackingToken trackingToken) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void storeSnapshot(@Nonnull DomainEventMessage snapshot) {
            // A dedicated implementation is not necessary for test fixture.
        }

        @Nonnull
        @Override
        public Registration subscribe(@Nonnull Consumer>> eventProcessor) {
            return () -> true;
        }

        @Override
        public @Nonnull
        Registration registerDispatchInterceptor(
                @Nonnull MessageDispatchInterceptor> dispatchInterceptor) {
            return () -> true;
        }
    }

    private static class ExecutionExceptionAwareCallback implements CommandCallback {

        private FixtureExecutionException exception;

        @Override
        public void onResult(@Nonnull CommandMessage commandMessage,
                             @Nonnull CommandResultMessage commandResultMessage) {
            if (commandResultMessage.isExceptional()) {
                Throwable cause = commandResultMessage.exceptionResult();
                if (cause instanceof FixtureExecutionException) {
                    this.exception = (FixtureExecutionException) cause;
                } else {
                    this.exception = new FixtureExecutionException("Failed to execute givenCommands", cause);
                }
            }
        }

        public void assertSuccessful() {
            if (exception != null) {
                throw exception;
            }
        }
    }

    private class DefaultRepositoryProvider implements RepositoryProvider {

        @Override
        public  Repository repositoryFor(@Nonnull Class aggregateType) {
            return new CreationalRepository<>(aggregateType, this);
        }
    }

    private class CreationalRepository implements Repository {

        private final Class aggregateType;
        private final RepositoryProvider repositoryProvider;

        private CreationalRepository(Class aggregateType,
                                     RepositoryProvider repositoryProvider) {
            this.aggregateType = aggregateType;
            this.repositoryProvider = repositoryProvider;
        }

        @Override
        public Aggregate load(@Nonnull String aggregateIdentifier) {
            throw new UnsupportedOperationException(
                    "Default repository does not mock loading of an aggregate, only creation of it");
        }

        @Override
        public Aggregate load(@Nonnull String aggregateIdentifier, Long expectedVersion) {
            throw new UnsupportedOperationException(
                    "Default repository does not mock loading of an aggregate, only creation of it");
        }

        @Override
        public Aggregate newInstance(@Nonnull Callable factoryMethod) throws Exception {
            AggregateModel aggregateModel = AnnotatedAggregateMetaModelFactory.inspectAggregate(aggregateType, getParameterResolverFactory(), getHandlerDefinition());
            return EventSourcedAggregate.initialize(factoryMethod, aggregateModel, eventStore, repositoryProvider);
        }

        @Override
        public void send(Message message, ScopeDescriptor scopeDescription) {
            throw new UnsupportedOperationException(
                    "Default repository does not mock loading of an aggregate, only creation of it");
        }

        @Override
        public boolean canResolve(ScopeDescriptor scopeDescription) {
            return false;
        }
    }
}