com.cqrs.testing.BddAggregateTestHelper Maven / Gradle / Ivy
package com.cqrs.testing;
import com.cqrs.aggregates.AggregateCommandHandlingException;
import com.cqrs.aggregates.AggregateEventApplyException;
import com.cqrs.aggregates.AggregateExecutionException;
import com.cqrs.aggregates.EventApplierOnAggregate;
import com.cqrs.annotations.MessageHandler;
import com.cqrs.base.Aggregate;
import com.cqrs.base.Command;
import com.cqrs.base.Event;
import com.cqrs.commands.CommandApplier;
import com.cqrs.commands.CommandHandlerNotFound;
import com.cqrs.commands.CommandSubscriber;
import com.cqrs.events.EventWithMetaData;
import com.cqrs.events.MetaData;
import com.cqrs.testing.exceptions.ExpectedEventNotYielded;
import com.cqrs.testing.exceptions.NoExceptionThrown;
import com.cqrs.testing.exceptions.TooManyEventsFired;
import com.cqrs.testing.exceptions.WrongExceptionClassThrown;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.time.LocalDateTime;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class BddAggregateTestHelper {
private final CommandApplier commandApplier;
private final CommandSubscriber commandSubscriber;
private String aggregateId;
private List priorEvents = new LinkedList<>();
private Command command;
private Aggregate aggregate;
public BddAggregateTestHelper(
CommandSubscriber commandSubscriber
) {
this.commandSubscriber = commandSubscriber;
commandApplier = new CommandApplier();
}
public static void assertEventListsAreEqual(List expectedEvents, List actualEvents
) throws ExpectedEventNotYielded, TooManyEventsFired {
List expected =
expectedEvents.stream().map(BddAggregateTestHelper::hashEvent).collect(Collectors.toList());
List actual =
actualEvents.stream().map(BddAggregateTestHelper::hashEvent).collect(Collectors.toList());
final ArrayList tooFew = new ArrayList<>(expected);
tooFew.removeAll(actual);
if (tooFew.size() > 0) {
throw new ExpectedEventNotYielded(tooFew.size() + " more events expected to be emitted: " + String.join("\n", tooFew));
}
final ArrayList tooMany = new ArrayList(actual);
tooMany.removeAll(expected);
if (tooMany.size() > 0) {
throw new TooManyEventsFired("Aggregate emitted too many events: " + String.join("\n", tooMany));
}
}
public static String hashEvent(Event event) {
return event.getClass().getCanonicalName() + ":" + StringDump.dump(event);
}
private boolean isClassOrSubClass(Class> parentClass, Class> childClass) {
return parentClass.isAssignableFrom(childClass);
}
public BddAggregateTestHelper onAggregate(Aggregate aggregate) {
this.aggregate = aggregate;
aggregateId = "123";
priorEvents = Collections.emptyList();
return this;
}
public BddAggregateTestHelper given(Event... priorEvents) {
this.priorEvents =
Arrays.stream(priorEvents).map(this::decorateEventWithMetaData).collect(Collectors.toList());
return this;
}
public BddAggregateTestHelper when(Command command) {
this.command = command;
return this;
}
public void then(Event... expectedEvents
) {
Objects.requireNonNull(command);
priorEvents.forEach(eventWithMetaData -> EventApplierOnAggregate.applyEvent(aggregate,
eventWithMetaData.event,
eventWithMetaData.metadata));
List newEvents;
try {
newEvents = executeCommand(command);
} catch (CommandHandlerNotFound | AggregateExecutionException e) {
throw new RuntimeException(e);
}
assertTheseEvents(Arrays.asList(expectedEvents), newEvents);
}
public void thenThrows(Class extends Throwable> expectedClass) {
try {
Objects.requireNonNull(command);
priorEvents.forEach(eventWithMetaData -> EventApplierOnAggregate.applyEvent(aggregate,
eventWithMetaData.event,
eventWithMetaData.metadata));
executeCommand(command);
throw new NoExceptionThrown("Aggregate was expected to throw " + expectedClass.getCanonicalName() + " but didn't");
} catch (AggregateExecutionException| AggregateEventApplyException| AggregateCommandHandlingException e) {
Throwable cause = e.getCause();
if (cause instanceof InvocationTargetException) {
cause = cause.getCause();
}
String causeName = cause.getClass().getCanonicalName();
if (!causeName.equals(expectedClass.getCanonicalName())) {
throw new WrongExceptionClassThrown("Aggregate was expected to throw " + expectedClass.getCanonicalName() + " but threw " + causeName);
}
} catch (NoExceptionThrown e) {
throw e;
} catch (Throwable e) {
throw new WrongExceptionClassThrown("Aggregate was expected to throw " + expectedClass.getCanonicalName() + " but threw " + e.getClass().getCanonicalName());
}
}
private List executeCommand(Command $command
) throws CommandHandlerNotFound, AggregateExecutionException {
MessageHandler handler = commandSubscriber.getAggregateForCommand(command.getClass());
return commandApplier.applyCommand(aggregate, $command, handler.methodName);
}
private EventWithMetaData decorateEventWithMetaData(Event event) {
return new EventWithMetaData(event, factoryMetaData());
}
public void assertTheseEvents(List expectedEvents, List actualEvents
) {
assertEventListsAreEqual(expectedEvents, actualEvents);
checkForToManyEvents(actualEvents.size() - expectedEvents.size());
}
private void checkForToManyEvents(int additionalCount) throws TooManyEventsFired {
if (additionalCount > 0) {
throw new TooManyEventsFired(
String.format("Additional %d events fired", additionalCount));
}
}
private MetaData factoryMetaData() {
return new MetaData(
LocalDateTime.now(),
aggregateId,
aggregate.getClass().getCanonicalName()
);
}
private static class StringDump {
/**
* Uses reflection and recursion to dump the contents of the given object using a custom, JSON-like notation (but not JSON). Does not format static fields.
*
* @param object the {@code Object} to dump using reflection and recursion
* @return a custom-formatted string representing the internal values of the parsed object
* @see #dump(Object, boolean, IdentityHashMap, int)
*/
public static String dump(Object object) {
return dump(object, false, new IdentityHashMap
© 2015 - 2025 Weber Informatics LLC | Privacy Policy