Hey guys!
I’ve been using Axon extensively for almost two years now and as I already posted in another topic, it is an awesome piece of product. I do have a probably silly question though. It’s about matching expected command in a saga test.
There are basically two approaches as to how aggregate identifiers are made and used as routing keys in commands.
- a client provided identifier
- the client code instantiates an identifier
- the client code instantiates a creational command with that known identifier
- the client code sends the command through a command gateway
- an aggregate handles that command and publishes its first event containing that identifier
- an aggregate applies that event and stores that identifier in a field annotated by
@AggregateIdentifier
- the command gateway returns the value of the annotated field, ie. the same identifier to the client code
- the client code uses either its own identifier instance or the one returned by the command gateway
- a domain provided identifier
- the client code instantiates a creational command with a dummy identifier, ie. some constant (or maybe
null
- here I am not sure how Axon handles it so I’ll stick with a constant) - the client code sends the command through a command gateway
- an aggregate handles that command and publishes its first event containing a newly generated identifier
- an aggregate applies that event and stores that identifier in a field annotated by
@AggregateIdentifier
- the command gateway returns the value of the annotated field, ie. the new identifier to the client code
- the client code then uses the identifier returned by the command gateway
- the client code instantiates a creational command with a dummy identifier, ie. some constant (or maybe
Then you have a saga which needs to create a new aggregate when some event happens on another aggregate (let’s say it is a different type). Testing such saga is pretty straightforward with the domain provided identifier because I can just expect and match commands with plain and simple equality and without the pain of guessing or mocking the identifier, because it is a known constant.
@SagaEventHandler(...)
void on(SomeFooEvent e, CommandGateway cw) {
cw.send(new CreateBarCommand(NEW_ID, e.getMessage());
}
@Test
void testCreate() {
new SagaTextFixture<MySaga>()
.givenNoPriorActivity()
.whenPublishingA(new SomeFooEvent(fooId, "hello"))
.expectDispatchedCommands(new CreateBarCommand(NEW_ID, "hello"));
}
With the client provided identifier, the situation becomes more tricky as far as my knowledge goes. The issue here is that the saga event handler code generates a random identifier and then creates a command with it.
This example will definitely not work:
@SagaEventHandler(...)
void on(SomeFooEvent e, CommandGateway cw) {
var barId = cw.sendAndWait(new CreateBarCommand(randomUUID(), e.getMessage());
associateWith("barId", barId.toString());
}
@Test
void testCreate() {
var fixture = new SagaTextFixture<MySaga>();
fixture.setCallbackBehavior((obj, metadata) -> randomUUID());
fixture
.givenNoPriorActivity()
.whenPublishingA(new SomeFooEvent(fooId, "hello"))
.expectDispatchedCommands(new CreateBarCommand(randomUUID(), "hello"));
}
I came up with a few strategies to solve the example above:
- use a matcher with ignored properties
- testing code becomes too complex
- testing code becomes too error prone to refactoring
- implement
CreateBarCommand.equals(Object)
to ignore the identifier field- defies the logic of equality
- delegate identifier generation to another class
- introduces one more class
- testing code becomes more complex because mocking and injecting the mocked resource
- capture the identifier in a callback behavior
- no changes to
equals()
- no introduction of one more class
- no mocking and injecting resources
- no changes to
Although I prefer the last strategy best as indicated by the thumbs, I can’t find any behavior in axon-test
that would make it happen, the DefaultCallbackBehavior
just returns null
. So I had to write one myself.
@Test
void testCreate() {
var capture = new CapturingBehavior<>(CreateBarCommand.class, UUID);
var fixture = new SagaTextFixture<MySaga>();
fixture.setCallbackBehavior(capture);
fixture
.givenNoPriorActivity()
.whenPublishingA(new SomeFooEvent(fooId, "hello"))
.expectDispatchedCommands(new CreateBarCommand(capture.getId(), "hello"));
}
class CapturingBehavior<T, R> implements CallbackBehavior {
private final Class<T> commandType;
private final Function<T, R> extractor;
private R identifier;
CapturingBehavior(Class<T> commandType, Function<T, R> extractor) {
this.commandType = commandType;
this.extractor = extractor;
}
public R getIdentifier() {
return identifier;
}
@Override
public Object handle(Object commandPayload, MetaData commandMetaData) throws Exception {
return Optional.ofNullable(commandPayload)
.filter(commandType::isInstance)
.map(commandType::cast)
.map(extractor.andThen(this::store))
.orElse(null);
}
private R store(R identifier) {
return this.identifier = identifier;
}
}
The code above is far from perfect but it provides two very nice features:
- first, it captures the client provided identifier for later use
- second, it mimics the default command gateway (or bus) by returning the identifier on specific commands
And here finally comes the real question…
Which strategy to test and match dispatched creational commands would you recommend?
Thanks for any feedback,
David