/** * Copyright (C) 2009-2016 Lightbend Inc. */ package akka.persistence.fsm; import akka.actor.*; import akka.japi.Option; import akka.persistence.PersistenceSpec; import akka.testkit.AkkaJUnitActorSystemResource; import akka.testkit.JavaTestKit; import akka.testkit.TestProbe; import org.junit.ClassRule; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import akka.persistence.fsm.PersistentFSM.CurrentState; import org.junit.Test; import scala.concurrent.duration.Duration; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.UserState; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.ShoppingCart; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.Item; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.GetCurrentCart; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.AddItem; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.Buy; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.Leave; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.PurchaseWasMade; import static akka.persistence.fsm.AbstractPersistentFSMTest.WebStoreCustomerFSM.ShoppingCardDiscarded; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.matchers.JUnitMatchers.hasItems; public class AbstractPersistentFSMTest { private static Option none = Option.none(); @ClassRule public static AkkaJUnitActorSystemResource actorSystemResource = new AkkaJUnitActorSystemResource("PersistentFSMJavaTest", PersistenceSpec.config( "leveldb", "AbstractPersistentFSMTest", "off", none.asScala())); private final ActorSystem system = actorSystemResource.getSystem(); //Dummy report actor, for tests that don't need it private final ActorRef dummyReportActorRef = new TestProbe(system).ref(); @Test public void fsmFunctionalTest() throws Exception { new JavaTestKit(system) {{ String persistenceId = generateId(); ActorRef fsmRef = system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef)); watch(fsmRef); fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); Item shirt = new Item("1", "Shirt", 59.99F); Item shoes = new Item("2", "Shoes", 89.99F); Item coat = new Item("3", "Coat", 119.99F); fsmRef.tell(GetCurrentCart.INSTANCE, getRef()); fsmRef.tell(new AddItem(shirt), getRef()); fsmRef.tell(GetCurrentCart.INSTANCE, getRef()); fsmRef.tell(new AddItem(shoes), getRef()); fsmRef.tell(GetCurrentCart.INSTANCE, getRef()); fsmRef.tell(new AddItem(coat), getRef()); fsmRef.tell(GetCurrentCart.INSTANCE, getRef()); fsmRef.tell(Buy.INSTANCE, getRef()); fsmRef.tell(GetCurrentCart.INSTANCE, getRef()); fsmRef.tell(Leave.INSTANCE, getRef()); CurrentState currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.LOOKING_AROUND); ShoppingCart shoppingCart = expectMsgClass(ShoppingCart.class); assertTrue(shoppingCart.getItems().isEmpty()); PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt)); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt, shoes)); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt, shoes, coat)); stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.SHOPPING, UserState.PAID); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt, shoes, coat)); Terminated terminated = expectMsgClass(Terminated.class); assertEquals(fsmRef, terminated.getActor()); }}; } @Test public void fsmTimeoutTest() throws Exception { new JavaTestKit(system) {{ String persistenceId = generateId(); ActorRef fsmRef = system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef)); watch(fsmRef); fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); Item shirt = new Item("1", "Shirt", 59.99F); fsmRef.tell(new AddItem(shirt), getRef()); CurrentState currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.LOOKING_AROUND); PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING); new Within(duration("0.9 seconds"), duration("1.9 seconds")) { @Override protected void run() { PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.SHOPPING, UserState.INACTIVE); } }; new Within(duration("1.9 seconds"), duration("2.9 seconds")) { @Override protected void run() { expectTerminated(fsmRef); } }; }}; } @Test public void testSuccessfulRecoveryWithCorrectStateData() { new JavaTestKit(system) {{ String persistenceId = generateId(); ActorRef fsmRef = system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef)); watch(fsmRef); fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); Item shirt = new Item("1", "Shirt", 59.99F); Item shoes = new Item("2", "Shoes", 89.99F); Item coat = new Item("3", "Coat", 119.99F); fsmRef.tell(GetCurrentCart.INSTANCE, getRef()); fsmRef.tell(new AddItem(shirt), getRef()); fsmRef.tell(GetCurrentCart.INSTANCE, getRef()); fsmRef.tell(new AddItem(shoes), getRef()); fsmRef.tell(GetCurrentCart.INSTANCE, getRef()); CurrentState currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.LOOKING_AROUND); ShoppingCart shoppingCart = expectMsgClass(ShoppingCart.class); assertTrue(shoppingCart.getItems().isEmpty()); PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt)); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt, shoes)); fsmRef.tell(PoisonPill.getInstance(), ActorRef.noSender()); expectTerminated(fsmRef); ActorRef recoveredFsmRef = system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef)); watch(recoveredFsmRef); recoveredFsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); recoveredFsmRef.tell(GetCurrentCart.INSTANCE, getRef()); recoveredFsmRef.tell(new AddItem(coat), getRef()); recoveredFsmRef.tell(GetCurrentCart.INSTANCE, getRef()); recoveredFsmRef.tell(Buy.INSTANCE, getRef()); recoveredFsmRef.tell(GetCurrentCart.INSTANCE, getRef()); recoveredFsmRef.tell(Leave.INSTANCE, getRef()); currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.SHOPPING); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt, shoes)); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt, shoes, coat)); stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, recoveredFsmRef, UserState.SHOPPING, UserState.PAID); shoppingCart = expectMsgClass(ShoppingCart.class); assertThat(shoppingCart.getItems(), hasItems(shirt, shoes, coat)); expectTerminated(recoveredFsmRef); }}; } @Test public void testExecutionOfDefinedActionsFollowingSuccessfulPersistence() { new JavaTestKit(system) {{ String persistenceId = generateId(); TestProbe reportActorProbe = new TestProbe(system); ActorRef fsmRef = system.actorOf(WebStoreCustomerFSM.props(persistenceId, reportActorProbe.ref())); watch(fsmRef); fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); Item shirt = new Item("1", "Shirt", 59.99F); Item shoes = new Item("2", "Shoes", 89.99F); Item coat = new Item("3", "Coat", 119.99F); fsmRef.tell(new AddItem(shirt), getRef()); fsmRef.tell(new AddItem(shoes), getRef()); fsmRef.tell(new AddItem(coat), getRef()); fsmRef.tell(Buy.INSTANCE, getRef()); fsmRef.tell(Leave.INSTANCE, getRef()); CurrentState currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.LOOKING_AROUND); PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING); stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.SHOPPING, UserState.PAID); PurchaseWasMade purchaseWasMade = reportActorProbe.expectMsgClass(PurchaseWasMade.class); assertThat(purchaseWasMade.getItems(), hasItems(shirt, shoes, coat)); expectTerminated(fsmRef); }}; } @Test public void testExecutionOfDefinedActionsFollowingSuccessfulPersistenceOfFSMStop() { new JavaTestKit(system) {{ String persistenceId = generateId(); TestProbe reportActorProbe = new TestProbe(system); ActorRef fsmRef = system.actorOf(WebStoreCustomerFSM.props(persistenceId, reportActorProbe.ref())); watch(fsmRef); fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); Item shirt = new Item("1", "Shirt", 59.99F); Item shoes = new Item("2", "Shoes", 89.99F); Item coat = new Item("3", "Coat", 119.99F); fsmRef.tell(new AddItem(shirt), getRef()); fsmRef.tell(new AddItem(shoes), getRef()); fsmRef.tell(new AddItem(coat), getRef()); fsmRef.tell(Leave.INSTANCE, getRef()); CurrentState currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.LOOKING_AROUND); PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING); reportActorProbe.expectMsgClass(ShoppingCardDiscarded.class); expectTerminated(fsmRef); }}; } @Test public void testCorrectStateTimeoutFollowingRecovery() { new JavaTestKit(system) {{ String persistenceId = generateId(); ActorRef fsmRef = system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef)); watch(fsmRef); fsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); Item shirt = new Item("1", "Shirt", 59.99F); fsmRef.tell(new AddItem(shirt), getRef()); CurrentState currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.LOOKING_AROUND); PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, fsmRef, UserState.LOOKING_AROUND, UserState.SHOPPING); expectNoMsg(duration("0.6seconds")); //randomly chosen delay, less than the timeout, before stopping the FSM fsmRef.tell(PoisonPill.getInstance(), ActorRef.noSender()); expectTerminated(fsmRef); final ActorRef recoveredFsmRef = system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef)); watch(recoveredFsmRef); recoveredFsmRef.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.SHOPPING); new Within(duration("0.9 seconds"), duration("1.9 seconds")) { @Override protected void run() { PersistentFSM.Transition stateTransition = expectMsgClass(PersistentFSM.Transition.class); assertTransition(stateTransition, recoveredFsmRef, UserState.SHOPPING, UserState.INACTIVE); } }; expectNoMsg(duration("0.9 seconds")); //randomly chosen delay, less than the timeout, before stopping the FSM recoveredFsmRef.tell(PoisonPill.getInstance(), ActorRef.noSender()); expectTerminated(recoveredFsmRef); final ActorRef recoveredFsmRef2 = system.actorOf(WebStoreCustomerFSM.props(persistenceId, dummyReportActorRef)); watch(recoveredFsmRef2); recoveredFsmRef2.tell(new PersistentFSM.SubscribeTransitionCallBack(getRef()), getRef()); currentState = expectMsgClass(akka.persistence.fsm.PersistentFSM.CurrentState.class); assertEquals(currentState.state(), UserState.INACTIVE); new Within(duration("1.9 seconds"), duration("2.9 seconds")) { @Override protected void run() { expectTerminated(recoveredFsmRef2); } }; }}; } private static void assertTransition(PersistentFSM.Transition transition, ActorRef ref, From from, To to) { assertEquals(ref, transition.fsmRef()); assertEquals(from, transition.from()); assertEquals(to, transition.to()); } private static String generateId() { return UUID.randomUUID().toString(); } public static class WebStoreCustomerFSM extends AbstractPersistentFSM { //State name //#customer-states enum UserState implements PersistentFSM.FSMState { LOOKING_AROUND("Looking Around"), SHOPPING("Shopping"), INACTIVE("Inactive"), PAID("Paid"); private final String stateIdentifier; UserState(String stateIdentifier) { this.stateIdentifier = stateIdentifier; } @Override public String identifier() { return stateIdentifier; } } //#customer-states //#customer-states-data static class ShoppingCart { private final List items = new ArrayList<>(); public List getItems() { return Collections.unmodifiableList(items); } void addItem(Item item) { items.add(item); } void empty() { items.clear(); } } static class Item implements Serializable { private final String id; private final String name; private final float price; Item(String id, String name, float price) { this.id = id; this.name = name; this.price = price; } public String getId() { return id; } public float getPrice() { return price; } public String getName() { return name; } @Override public String toString() { return String.format("Item{id=%s, name=%s, price=%s}", id, price, name); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Item item = (Item) o; return item.price == price && id.equals(item.id) && name.equals(item.name); } } //#customer-states-data public interface Command { } //#customer-commands public static final class AddItem implements Command { private final Item item; public AddItem(Item item) { this.item = item; } public Item getItem() { return item; } } public enum Buy implements Command {INSTANCE} public enum Leave implements Command {INSTANCE} public enum GetCurrentCart implements Command {INSTANCE} //#customer-commands interface DomainEvent extends Serializable { } //#customer-domain-events public static final class ItemAdded implements DomainEvent { private final Item item; public ItemAdded(Item item) { this.item = item; } public Item getItem() { return item; } } public enum OrderExecuted implements DomainEvent {INSTANCE} public enum OrderDiscarded implements DomainEvent {INSTANCE} //#customer-domain-events //Side effects - report events to be sent to some "Report Actor" public interface ReportEvent { } public static final class PurchaseWasMade implements ReportEvent { private final List items; public PurchaseWasMade(List items) { this.items = Collections.unmodifiableList(items); } public List getItems() { return items; } } public enum ShoppingCardDiscarded implements ReportEvent {INSTANCE} final private String persistenceId; @Override public Class domainEventClass() { return DomainEvent.class; } @Override public String persistenceId() { return persistenceId; } public static Props props(String persistenceId, ActorRef reportActor) { return Props.create(WebStoreCustomerFSM.class, persistenceId, reportActor); } public WebStoreCustomerFSM(String persistenceId, ActorRef reportActor) { this.persistenceId = persistenceId; //#customer-fsm-body startWith(UserState.LOOKING_AROUND, new ShoppingCart()); when(UserState.LOOKING_AROUND, matchEvent(AddItem.class, (event, data) -> goTo(UserState.SHOPPING).applying(new ItemAdded(event.getItem())) .forMax(Duration.create(1, TimeUnit.SECONDS)) ) .event(GetCurrentCart.class, (event, data) -> stay().replying(data)) ); when(UserState.SHOPPING, matchEvent(AddItem.class, (event, data) -> stay().applying(new ItemAdded(event.getItem())) .forMax(Duration.create(1, TimeUnit.SECONDS))) .event(Buy.class, (event, data) -> goTo(UserState.PAID).applying(OrderExecuted.INSTANCE) .andThen(exec(cart -> reportActor.tell(new PurchaseWasMade(cart.getItems()), self())) )) .event(Leave.class, (event, data) -> stop().applying(OrderDiscarded.INSTANCE) .andThen(exec(cart -> reportActor.tell(ShoppingCardDiscarded.INSTANCE, self()) ))) .event(GetCurrentCart.class, (event, data) -> stay().replying(data)) .event(StateTimeout$.class, (event, data) -> goTo(UserState.INACTIVE).forMax(Duration.create(2, TimeUnit.SECONDS))) ); when(UserState.INACTIVE, matchEvent(AddItem.class, (event, data) -> goTo(UserState.SHOPPING).applying(new ItemAdded(event.getItem())) .forMax(Duration.create(1, TimeUnit.SECONDS))) .event(GetCurrentCart.class, (event, data) -> stay().replying(data)) .event(StateTimeout$.class, (event, data) -> stop().applying(OrderDiscarded.INSTANCE) .andThen(exec(cart -> reportActor.tell(ShoppingCardDiscarded.INSTANCE, self()) ))) ); when(UserState.PAID, matchEvent(Leave.class, (event, data) -> stop()) .event(GetCurrentCart.class, (event, data) -> stay().replying(data)) ); initialize(); //#customer-fsm-body } /** * Override this handler to define the action on Domain Event during recovery * * @param event domain event to apply * @param currentData state data of the previous state */ //#customer-apply-event @Override public ShoppingCart applyEvent(DomainEvent event, ShoppingCart currentData) { if (event instanceof ItemAdded) { currentData.addItem(((ItemAdded) event).getItem()); return currentData; } else if (event instanceof OrderExecuted) { return currentData; } else if (event instanceof OrderDiscarded) { currentData.empty(); return currentData; } throw new RuntimeException("Unhandled"); } //#customer-apply-event } }