Commands matching in saga tests

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

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 :-1:
    • testing code becomes too complex
    • testing code becomes too error prone to refactoring
  • implement CreateBarCommand.equals(Object) to ignore the identifier field :-1:
    • defies the logic of equality
  • delegate identifier generation to another class :-1:
    • introduces one more class
    • testing code becomes more complex because mocking and injecting the mocked resource
  • capture the identifier in a callback behavior :+1:
    • no changes to equals()
    • no introduction of one more class
    • no mocking and injecting resources

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

Hi David,

I actually like this concept. It’s a more elegant solution than using Matchers (for all except 1 field) or a custom Random Value Generator.

While the callback behavior wasn’t primarily intended for this, it will actually work. The intent of the CallbackBehavior class is to ensure the Commands yield the expected response when the Saga dispatches a command. Since it’s best practice for commands to return just null (basically serving as an acknowledgement of processing), that’s what it returns by default.

Using a DispatchInterceptor mechanism is probably best here. You should be able to register one using fixture.getCommandBus().registerDispatchInterceptor(). And then instead of modifying the outgoing command, simply extract some information from it. The advantage is that interceptors are easier to chain. Simple register one for each type of command that you’d like to extract information from.

I think this is an instance of a more generic problem: testing methods which are using static calls.

As you mentioned it appears when using randomUUID(). Another example is when you use Instant.now() (which is quite common I think)

The way I deal with that is:

  1. create an interface which is effectively a supplier for the type.
  2. have that type as an argument to the method.
  3. have tests inject the desired implementation.

I use specialised interfaces to avoid potential problems with erasure which could occur later. Example:

@FunctionalInterface
public interface UUIDSupplier extends Supplier<UUID> {}

@FunctionalInterface
public interface InstantSupplier extends Supplier<Instant> {}

Then you can use it like so:

@SagaEventHandler(...)
void on(SomeFooEvent e, CommandGateway cw, UUIDSupplier uuidSupplier) {
   var uuid = uuidSupplier.get();
   var barId = cw.sendAndWait(new CreateBarCommand(uuid, e.getMessage());
   associateWith("barId", barId.toString());
}

And for tests:

@Test
void testCreate() {
  var fixture = new SagaTextFixture<MySaga>();
  var uuidSupplier = () -> UUID.randomUUID();
  fixture.registerResource(uuidSupplier);
  fixture
   .givenNoPriorActivity()
   .whenPublishingA(new SomeFooEvent(fooId, "hello"))
   .expectDispatchedCommands(new CreateBarCommand(uuidSupplier.get(), "hello"));
}

Obviously for production you would need to provide a bean which can be injected to give an actual random UUID.

Which one you prefer is your preference - you have said you don’t like injecting ‘mocked’ resources so you may not like this method but I thought I would share.

I may have made some mistakes typing this up (I haven’t tested the code compiles) but I hope you get the gist…

1 Like