Skip to content

Commit

Permalink
Created stub implementation of AggregateLifecycle that can be used in…
Browse files Browse the repository at this point in the history
… tests

Especially in unit tests of aggregates that do not use event sourcing.
When they call apply(), and exception is thrown indicating that no
lifecycle has been registered. The StubAggregateLifecycle can be used
to register a lifecycle that just keeps track of applied events.

In JUnit tests, the StubAggregateLifecycleRule can be used to
automatically start these lifecycles during tests.

Resolves #336
  • Loading branch information
abuijze committed Nov 22, 2017
1 parent bc8747e commit eea66c9
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 14 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2016. Axon Framework
* Copyright (c) 2010-2017. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -79,15 +79,6 @@ public static boolean isLive() {
return AggregateLifecycle.getInstance().getIsLive();
}

/**
* Indicates whether this Aggregate instance is 'live'. This means events currently applied represent events that
* are currently happening, as opposed to events representing historic decisions.
*
* @return {@code true} if the aggregate is 'live', {@code false} if the aggregate is initializing state based on
* historic events
*/
protected abstract boolean getIsLive();

/**
* Marks this aggregate as deleted, instructing a repository to remove that aggregate at an appropriate time.
* <p/>
Expand Down Expand Up @@ -120,6 +111,15 @@ protected static AggregateLifecycle getInstance() {
return instance;
}

/**
* Indicates whether this Aggregate instance is 'live'. This means events currently applied represent events that
* are currently happening, as opposed to events representing historic decisions.
*
* @return {@code true} if the aggregate is 'live', {@code false} if the aggregate is initializing state based on
* historic events
*/
protected abstract boolean getIsLive();

/**
* Marks this aggregate as deleted. Implementations may react differently to aggregates marked for deletion.
* Typically, Event Sourced Repositories will ignore the marking and expect deletion to be provided as part of Event
Expand Down Expand Up @@ -161,17 +161,30 @@ protected void registerWithUnitOfWork() {
* @throws Exception if executing the task causes an exception
*/
protected <V> V executeWithResult(Callable<V> task) throws Exception {
AggregateLifecycle existing = CURRENT.get();
CURRENT.set(this);
Runnable handle = registerAsCurrent();
try {
return task.call();
} finally {
handle.run();
}
}

/**
* Registers the current AggregateLifecycle as the current lifecycle. The returned Runnable should be executed to
* restore the lifecycle to the previous state.
*
* @return a runnable that must be executed to return the lifecycle to the original state
*/
protected Runnable registerAsCurrent() {
AggregateLifecycle existing = CURRENT.get();
CURRENT.set(this);
return () -> {
if (existing == null) {
CURRENT.remove();
} else {
CURRENT.set(existing);
}
}
};
}

/**
Expand Down
8 changes: 7 additions & 1 deletion test/pom.xml
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2010-2014. Axon Framework
~ Copyright (c) 2010-2017. Axon Framework
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -33,6 +33,12 @@
<artifactId>axon-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
Expand Down
@@ -0,0 +1,122 @@
/*
* Copyright (c) 2010-2017. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.axonframework.test.aggregate;

import org.axonframework.commandhandling.model.AggregateLifecycle;
import org.axonframework.commandhandling.model.ApplyMore;
import org.axonframework.eventhandling.EventMessage;
import org.axonframework.eventhandling.GenericEventMessage;
import org.axonframework.messaging.MetaData;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
* Stub implementation of an AggregateLifecycle that registers all applied events for verification later. This
* lifecycle instance can be activated (see {@link #activate()}) and deactivated (see {@link #close()}) at will. Events
* applied while it is active are stored and can be retrieved using {@link #getAppliedEvents()} or
* {@link #getAppliedEventPayloads()}.
* <p>
* When using with JUnit, consider using the {@link StubAggregateLifecycleRule} with {@link org.junit.Rule &#064;Rule} instead,
* as it is easier and safer to use.
*/
public class StubAggregateLifecycle extends AggregateLifecycle {

private Runnable registration;
private List<EventMessage<?>> appliedMessages = new CopyOnWriteArrayList<>();
private boolean deleted;

/**
* Activates this lifecycle instance. Any invocations to static AggregateLifecycle methods will use this instance
* until {@link #close()} is called.
*/
public void activate() {
this.registration = registerAsCurrent();
}

/**
* Closes this lifecycle instance, restoring to the situation prior to this lifecycle being started. If any
* lifecycle instance was active before this one started, it will be reactivated.
*/
public void close() {
if (registration != null) {
registration.run();
}
registration = null;
}

@Override
protected boolean getIsLive() {
return true;
}

@Override
protected void doMarkDeleted() {
this.deleted = true;
}

@Override
protected <T> ApplyMore doApply(T payload, MetaData metaData) {
appliedMessages.add(new GenericEventMessage<>(payload, metaData));

return new ApplyMore() {
@Override
public ApplyMore andThenApply(Supplier<?> payloadOrMessageSupplier) {
appliedMessages.add(GenericEventMessage.asEventMessage(payloadOrMessageSupplier.get()));
return this;
}

@Override
public ApplyMore andThen(Runnable runnable) {
runnable.run();
return this;
}
};
}

/**
* Returns the list of applied Events for this lifecycle instance.
* <p>
* Note that this list is not reset when activating or deactivating the lifecycle.
*
* @return the list of messages applied while this lifecycle instance was active
*/
public List<EventMessage<?>> getAppliedEvents() {
return appliedMessages;
}

/**
* Returns the payloads of the Events applied while this lifecycle instance was active. This is usually the
* parameter passed to the {@link AggregateLifecycle#apply(Object)} method.
*
* @return the payloads of the applied events.
*/
public List<Object> getAppliedEventPayloads() {
return appliedMessages.stream().map(EventMessage::getPayload).collect(Collectors.toList());
}

/**
* Indicates whether an Aggregate has invoked "markDeleted" while this lifecycle was active.
*
* @return {@code true} if {@link #markDeleted()} was invoked, otherwise {@code false}
*/
public boolean isMarkedDeleted() {
return deleted;
}
}
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2010-2017. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.axonframework.test.aggregate;

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

/**
* Implementation of StubAggregateLifecycle that can be used as an {@link org.junit.Rule} annotated method or field
* in a test class. In that case, the JUnit lifecycle will automatically register and unregister the
* StubAggregateLifecycle.
*
* Usage example:
* <pre>
* &#064;Rule
* public StubAggregateLifecycleRule lifecycle = new StubAggregateLifecycleRule();
*
* &#064;Test
* public void testMethod() {
* ... perform tests ...
*
* // get applied events from lifecycle to validate some more
* lifecycle.getAppliedEvents();
* }
* </pre>
*/
public class StubAggregateLifecycleRule extends StubAggregateLifecycle implements TestRule {

@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
activate();
try {
base.evaluate();
} finally {
close();
}
}
};
}

}
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2010-2017. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.axonframework.test.aggregate;

import org.junit.Rule;
import org.junit.Test;

import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
import static org.axonframework.commandhandling.model.AggregateLifecycle.markDeleted;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class StubAggregateLifecycleRuleTest {

@Rule
public StubAggregateLifecycleRule testSubject = new StubAggregateLifecycleRule();

@Test
public void testAppliedEventsArePassedToActiveLifecycle() throws Exception {
apply("test");

assertEquals(1, testSubject.getAppliedEvents().size());
assertEquals("test", testSubject.getAppliedEventPayloads().get(0));
assertEquals("test", testSubject.getAppliedEvents().get(0).getPayload());
}

@Test
public void testMarkDeletedIsRegisteredWithActiveLifecycle() throws Exception {
markDeleted();

assertEquals(0, testSubject.getAppliedEvents().size());
assertTrue(testSubject.isMarkedDeleted());
}

}
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2010-2017. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.axonframework.test.aggregate;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.axonframework.commandhandling.model.AggregateLifecycle.apply;
import static org.axonframework.commandhandling.model.AggregateLifecycle.markDeleted;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class StubAggregateLifecycleTest {
private StubAggregateLifecycle testSubject;

@Before
public void setUp() throws Exception {
testSubject = new StubAggregateLifecycle();
}

@After
public void tearDown() throws Exception {
testSubject.close();
}

@Test(expected = IllegalStateException.class)
public void testLifecycleIsNotRegisteredAutomatically() throws Exception {
apply("test");
}

@Test(expected = IllegalStateException.class)
public void testApplyingEventsAfterDeactivationFails() throws Exception {
testSubject.activate();
testSubject.close();
apply("test");
}

@Test
public void testAppliedEventsArePassedToActiveLifecycle() throws Exception {
testSubject.activate();
apply("test");

assertEquals(1, testSubject.getAppliedEvents().size());
assertEquals("test", testSubject.getAppliedEventPayloads().get(0));
assertEquals("test", testSubject.getAppliedEvents().get(0).getPayload());
}

@Test
public void testMarkDeletedIsRegisteredWithActiveLifecycle() throws Exception {
testSubject.activate();
markDeleted();

assertEquals(0, testSubject.getAppliedEvents().size());
assertTrue(testSubject.isMarkedDeleted());
}
}

0 comments on commit eea66c9

Please sign in to comment.