Hi,
we are researching/learning Axon framework, and we stumbled upon one problem.
We have saga that orchestrates some glue logic between 2 aggregates. Event that triggers saga contains aggregate ids. In order to get some details needed for saga, we load aggregate instances using aggregate ids via event repository. It all works fine when application is started (within Spring container).
But, when we want to make test fixtures, we can’t inject event repository. At least I couldn’t find the way to do it.
If we mock repository, and we return stub aggregates for mocked repository load calls, it still does not work since ofc, state of stub aggregates does not get ‘affected’ by playing the events in fixture.
So, if we could ‘access’ somehow real aggregate instances behind fixture, we could connect them to mocked repository. But preferably, it would be great if it would work without need to mock repository.
Event repository that we use is just some handy proxy around org.axonframework.commandhandling.model.Repository, and it depends on @Autowired AxonConfiguration instance, that is also not available within test scope.
To illustrate, here is saga code:
@Saga
public class TenantApartmentApplicationSaga {
@Inject
private transient CommandGateway commandGateway;
@Inject
private transient EventRepositoryFactory factory;
private transient EventRepository<Tenant> repoTenant;
private transient EventRepository<Building> repoBuilding;
private TenantId tenantId;
@SagaEventHandler(associationProperty = TenantAppliedForApartmentEvent.APARTMENT_ID)
@StartSaga
public void on(TenantAppliedForApartmentEvent event) {
tenantId = event.getTenantId();
Tenant tenant = getRepoTenant().load(tenantId.getId());
Building building = getRepoBuilding().load(event.getBuildingId().getId());
Pair<Integer, Integer> apartmentData = building.getApartmentFloorAndNumber(event.getApartmentId());
String messageText = String.format(
"Tenant %s applies for apartment number %d of floor %d at building on address %s", tenant.getName(),
apartmentData.getFirst(), apartmentData.getSecond(), building.getAddress());
commandGateway.send(
new StartConversationCommand(event.getTenantId(), Arrays.asList(building.getBuildingManagerId()), messageText),
LoggingCallback.INSTANCE);
}
@SagaEventHandler(associationProperty = TenantApplicationForApartmentApprovedEvent.APARTMENT_ID)
public void on(TenantApplicationForApartmentApprovedEvent event) {
if (tenantId.equals(event.getTenantId())) {
end();
}
}
@SagaEventHandler(associationProperty = TenantApplicationForApartmentRefusedEvent.APARTMENT_ID)
public void on(TenantApplicationForApartmentRefusedEvent event) {
if (tenantId.equals(event.getTenantId())) {
end();
}
}
private EventRepository<Tenant> getRepoTenant() {
if (repoTenant == null) {
repoTenant = factory.createEventFactory(Tenant.class);
}
return repoTenant;
}
private EventRepository<Building> getRepoBuilding() {
if (repoBuilding == null) {
repoBuilding = factory.createEventFactory(Building.class);
}
return repoBuilding;
}
Test that has problem looks like following.
Variant where we do not mock repositories / factories:
@RunWith(MockitoJUnitRunner.class)
public class TenantApartmentApplicationSagaTest {
private FixtureConfiguration fixture;
@Before
public void setUp() {
fixture = new SagaTestFixture<>(TenantApartmentApplicationSaga.class);
}
@Test
public void testTenantApartmentApplicationSaga_whenTenantAppliedForApartment_expectStartConversationCommand() {
BuildingId buildingId = create(BuildingId.class, UUID.randomUUID());
Building building = BuildingMother.building(buildingId, 3, 21);
Apartment apartment = ApartmentMother.apartment(3, 20, buildingId);
Tenant tenant = UserMother.tenant();
BuildingManager buildingManager = UserMother.buildingManager();
ManagerRegisteredEvent managerRegisteredEvent = from(buildingManager);
BuildingRegisteredEvent buildingRegisteredEvent = from(buildingId, buildingManager.getId(), building);
TenantRegisteredEvent tenantRegisteredEvent = from(tenant);
ApartmentAddedEvent newApartmentAddedEvent = ApartmentAddedEventFactory.from(apartment);
TenantAppliedForApartmentEvent tenantAppliedForApartmentEvent = new TenantAppliedForApartmentEvent(
buildingId, apartment.getId(), tenant.getId());
fixture.givenAggregate(buildingManager.getId().getId())
.published(managerRegisteredEvent)
.andThenAggregate(building.getId().getId())
.published(buildingRegisteredEvent)
.andThenAggregate(tenant.getId().getId())
.published(tenantRegisteredEvent)
.andThenAggregate(building.getId().getId())
.published(newApartmentAddedEvent)
.whenAggregate(building.getId().getId())
.publishes(tenantAppliedForApartmentEvent)
.expectDispatchedCommandsMatching(
exactSequenceOf(
StartConversationCommandMatcher.newInstance(tenant.getId())
)
).expectActiveSagas(1);
}
}
Fails with NPE:
Caused by: java.lang.NullPointerException
at eu.execom.buildingmanager.domain.building.sagas.TenantApartmentApplicationSaga.getRepoTenant(TenantApartmentApplicationSaga.java:70)
because it can’t inject automatically that EventRepositoryFactory instance, and field remains null.
However, other variant of test that registers injectable object as mocked factory (and repositories):
@RunWith(MockitoJUnitRunner.class)
public class TenantApartmentApplicationSagaTest {
private FixtureConfiguration fixture;
@Mock
private EventRepositoryFactory factory;
@Mock
private EventRepository<Tenant> tenantRepo;
@Mock
private EventRepository<Building> buildingRepo;
@Before
public void setUp() {
fixture = new SagaTestFixture<>(TenantApartmentApplicationSaga.class);
fixture.registerResource(factory);
}
@Test
public void testTenantApartmentApplicationSaga_whenTenantAppliedForApartment_expectStartConversationCommand() {
BuildingId buildingId = create(BuildingId.class, UUID.randomUUID());
Building building = BuildingMother.building(buildingId, 3, 21);
Apartment apartment = ApartmentMother.apartment(3, 20, buildingId);
Tenant tenant = UserMother.tenant();
BuildingManager buildingManager = UserMother.buildingManager();
ManagerRegisteredEvent managerRegisteredEvent = from(buildingManager);
BuildingRegisteredEvent buildingRegisteredEvent = from(buildingId, buildingManager.getId(), building);
TenantRegisteredEvent tenantRegisteredEvent = from(tenant);
ApartmentAddedEvent newApartmentAddedEvent = ApartmentAddedEventFactory.from(apartment);
TenantAppliedForApartmentEvent tenantAppliedForApartmentEvent = new TenantAppliedForApartmentEvent(
buildingId, apartment.getId(), tenant.getId());
when(factory.createEventFactory(Tenant.class)).thenReturn(tenantRepo);
when(factory.createEventFactory(Building.class)).thenReturn(buildingRepo);
when(tenantRepo.load(tenant.getId().getId())).thenReturn(tenant);
when(buildingRepo.load(buildingId.getId())).thenReturn(building);
fixture.givenAggregate(buildingManager.getId().getId())
.published(managerRegisteredEvent)
.andThenAggregate(building.getId().getId())
.published(buildingRegisteredEvent)
.andThenAggregate(tenant.getId().getId())
.published(tenantRegisteredEvent)
.andThenAggregate(building.getId().getId())
.published(newApartmentAddedEvent)
.whenAggregate(building.getId().getId())
.publishes(tenantAppliedForApartmentEvent)
.expectDispatchedCommandsMatching(
exactSequenceOf(
StartConversationCommandMatcher.newInstance(tenant.getId())
)
).expectActiveSagas(1);
}
}
Fails with problem that building aggregate that what we return as mocked repository load method result, is not what is actually maintained by axon under the fixture hood when plays the specified events… Exception is clear (well it is not readable from posted parts of the code, but by playing newApartmentAddedEvent an Apartment entity should be stored into local List field within Building aggregate, and that actually does happen - but in real aggregate within fixture, that we can’t access from outside (test code).
Caused by: java.lang.IllegalArgumentException: Apartment not found for apartmentId 4809da9c-97dc-4782-ac95-861d22e78eb3 in building 1192bc0c-150c-462d-8701-a7a84a95e442
at eu.execom.buildingmanager.domain.building.Building.getApartmentFloorAndNumber(Building.java:311)
at eu.execom.buildingmanager.domain.building.sagas.TenantApartmentApplicationSaga.on(TenantApartmentApplicationSaga.java:45)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.axonframework.messaging.annotation.AnnotatedMessageHandlingMember.handle(AnnotatedMessageHandlingMember.java:127)
So, what is the proposed way to solve this? Is it possible to access somehow real aggregates under the fixture, or to somehow configure/initialize some instance of AxonConfiguration within test and wrap event repository around it, or something else? I hope it is not meant that within Saga we can’t access aggregates via repository and to force all the data to be exposed only via event fields. Or even worse, to be forced to maintain in test code also stub objects to act as event sourcing handlers were handling them - then it is like rewriting the logic within the test code.
Let me know if I should clarify any parts of my question, and I hope I just miss some well known practice - since that seems like some standard use scenario to me
Thanks for help,
Vladimir