GoldFlsh
GoldFlsh

Reputation: 1334

How Can I Test Aggregate if ID is randomly generated?

This may be more of a design question but I have an aggregate member that is generated via a command and need to be able to test that the Event is generated given the command was run.

however, I don't see any obvious way to do an anyString match on one field of the event in the TestFixture framework.

Is it "bad practice" to generate IDs in the aggregate when created? Should IDs be generated outside of the aggregate?

@AggregateMember(eventForwardingMode = ForwardMatchingInstances.class)
private List<TimeCardEntry> timeCardEntries = new ArrayList<>();

data class ClockInCommand(@TargetAggregateIdentifier val employeeName: String)

@CommandHandler
public TimeCard(ClockInCommand cmd) {
  apply(new ClockInEvent(cmd.getEmployeeName(),
      GenericEventMessage.clock.instant(),
      UUID.randomUUID().toString()));


@EventSourcingHandler
public void on(ClockInEvent event) {
  this.employeeName = event.getEmployeeName();
  timeCardEntries.add(new TimeCardEntry(event.getTimeCardEntryId(), event.getTime()));
}

@Data
public class TimeCardEntry {

  @EntityId
  private final String timeCardEntryId;
  private final Instant clockInTime;
  private Instant clockOutTime;

  @EventSourcingHandler
  public void on(ClockOutEvent event) {
    this.clockOutTime = event.getTime();
  }

  private boolean isClockedIn() {
    return clockOutTime != null;
  }
}

@ParameterizedTest
@MethodSource(value = "randomEmployeeName")
void testClockInCommand(String employeeName) {
    testFixture.givenNoPriorActivity()
            .when(new ClockInCommand(employeeName))
            .expectEvents(new ClockInEvent(employeeName, testFixture.currentTime(), "Any-String-Works"));
}

Upvotes: 0

Views: 568

Answers (1)

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57279

Is it "bad practice" to generate IDs in the aggregate when created? Should IDs be generated outside of the aggregate?

Random numbers are a lot like clocks - they are a form of shared mutable state. Put another way, they are a concern of the imperative shell, not of the functional core.

What this usually means for your domain model is that the randomness is passed in as an argument, rather than produced by the aggregate itself. This might mean passing an ID generator to the domain model, or even generating the id in the application and passing in the generated identifier as a value.

Thus, in our unit test, we replace the random generator provided by the target application with a "random" generator provided by the test -- because the test controls the generator, the identifier used becomes deterministic, and therefore you can more easily work around it.

In cases where you aren't happy with making the random generator part of the api of your domain model, another option is to expose it as part of the test interface.

// We don't necessarily worry about testing this version, it is "too simple to break"
void doSomethingCool(...) {
    doSomethingCool(ID.new, ...);
}

// Unit tests measure this function instead, which is easier to test and has
// all of the complicated logic
void doSomethingCool(ID id, ...) {
    // ...
}

Upvotes: 4

Related Questions