/** * Constrains the flow's condition: only if the specified condition is true, the * flow is started. * * @param condition * the condition that constrains when the flow is started * @return the condition part, to ease creation of the first step of the flow */ public ConditionPart condition(Condition condition) { Objects.requireNonNull(condition); optionalFlowPositionPart = new FlowPositionPart(new Anytime(), this); ConditionPart conditionPart = optionalFlowPositionPart.condition(condition); return conditionPart; }
/** * Creates the first step of this flow. It can be run when the runner is at the * right position and the flow's condition is fulfilled. * * @param stepName * the name of the step to be created * @return the newly created step part, to ease creation of further steps * @throws ElementAlreadyInModel * if a step with the specified name already * exists in the use case */ public StepPart step(String stepName) { return FlowPositionPart.this.step(stepName); } }
public Model buildWith(ModelBuilder modelBuilder) { normalUser = modelBuilder.actor("Normal User"); anonymousUser = modelBuilder.actor("Anonymous User"); Model model = modelBuilder.useCase("Get greeted") .basicFlow() .step("S1").as(normalUser).system(this::promptsUserToEnterFirstName) .step("S2").as(normalUser).user(ENTERS_FIRST_NAME).system(this::savesFirstName) .step("S3").as(normalUser, anonymousUser).system(this::promptsUserToEnterAge) .step("S4").as(normalUser, anonymousUser).user(ENTERS_AGE).system(this::savesAge) .step("S5").as(normalUser).system(this::greetsUserWithFirstName) .step("S6").as(normalUser, anonymousUser).system(this::greetsUserWithAge) .step("S7").as(normalUser, anonymousUser).system(this::stops) .flow("Handle out-of-bounds age").insteadOf("S5").condition(this::ageIsOutOfBounds) .step("S5a_1").system(this::informsUserAboutOutOfBoundsAge) .step("S5a_2").continuesAt("S3") .flow("Handle non-numerical age").insteadOf("S5") .step("S5b_1").on(NON_NUMERICAL_AGE).system(this::informsUserAboutNonNumericalAge) .step("S5b_2").continuesAt("S3") .flow("Anonymous greeted with age only").insteadOf("S5").condition(this::ageIsOk) .step("S5c_1").as(anonymousUser).continuesAt("S6") .flow("Anonymous does not enter name").insteadOf("S1") .step("S1a_1").as(anonymousUser).continuesAt("S3") .build(); return model; }
@Test public void continuesAfterCalledFromMultipleMutuallyExclusiveAlternativeFlows() { Model model = modelBuilder .useCase(USE_CASE) .basicFlow() .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) .step(CUSTOMER_ENTERS_TEXT_AGAIN).user(EntersText.class).system(displaysEnteredText()) .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) .flow(ALTERNATIVE_FLOW).insteadOf(CUSTOMER_ENTERS_TEXT_AGAIN).condition(this::textIsAvailable) .step(CUSTOMER_ENTERS_ALTERNATIVE_TEXT).user(EntersText.class).system(displaysEnteredText()) .step(CONTINUE).continuesAfter(CUSTOMER_ENTERS_TEXT_AGAIN) .flow(ALTERNATIVE_FLOW_2).insteadOf(CUSTOMER_ENTERS_TEXT_AGAIN).condition(this::textIsNotAvailable) .step("Customer enters alternative number").user(EntersNumber.class).system(displaysEnteredNumber()) .step(CONTINUE_2).continuesAfter(CUSTOMER_ENTERS_TEXT_AGAIN) .build(); modelRunner.run(model).reactTo(entersText(), entersAlternativeText(), entersText(), entersNumber()); assertRecordedStepNames(CUSTOMER_ENTERS_TEXT, CUSTOMER_ENTERS_ALTERNATIVE_TEXT, CONTINUE, CUSTOMER_ENTERS_NUMBER); }
/** * Starts the flow after any step that has been run, or at the beginning. * * @return the flow position part, to ease creation of the condition and the * first step of the flow */ public FlowPositionPart anytime() { Anytime anytime = new Anytime(); optionalFlowPositionPart = new FlowPositionPart(anytime, this); return optionalFlowPositionPart; }
public Model buildWith(ModelBuilder modelBuilder) { Model model = modelBuilder.useCase("Get greeted") .basicFlow() .step("S1").system(this::promptsUserToEnterFirstName) .step("S2").user(ENTERS_FIRST_NAME).system(this::savesFirstName) .step("S3").system(this::promptsUserToEnterAge) .step("S4").user(ENTERS_AGE).system(this::savesAge) .step("S5").system(this::greetsUserWithFirstNameAndAge) .step("S6").system(this::stops) .flow("Handle out-of-bounds age").insteadOf("S5").condition(this::ageIsOutOfBounds) .step("S5a_1").system(this::informsUserAboutOutOfBoundsAge) .step("S5a_2").continuesAt("S3") .flow("Handle non-numerical age").insteadOf("S5") .step("S5b_1").on(NON_NUMERICAL_AGE).system(this::informsUserAboutNonNumericalAge) .step("S5b_2").continuesAt("S3") .build(); return model; }
@Test public void continuesAtCalledFromMultipleMutuallyExclusiveAlternativeFlows() { Model model = modelBuilder .useCase(USE_CASE) .basicFlow() .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) .step(CUSTOMER_ENTERS_TEXT_AGAIN).user(EntersText.class).system(displaysEnteredText()) .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) .flow(ALTERNATIVE_FLOW).insteadOf(CUSTOMER_ENTERS_TEXT_AGAIN).condition(this::textIsAvailable) .step(CUSTOMER_ENTERS_ALTERNATIVE_TEXT).user(EntersText.class).system(displaysEnteredText()) .step(CONTINUE).continuesAt(CUSTOMER_ENTERS_NUMBER) .flow(ALTERNATIVE_FLOW_2).insteadOf(CUSTOMER_ENTERS_TEXT_AGAIN).condition(this::textIsNotAvailable) .step("Customer enters alternative number").user(EntersNumber.class).system(displaysEnteredNumber()) .step(CONTINUE_2).continuesAt(CUSTOMER_ENTERS_NUMBER) .build(); modelRunner.run(model).reactTo(entersText(), entersAlternativeText(), entersText(), entersNumber()); assertRecordedStepNames(CUSTOMER_ENTERS_TEXT, CUSTOMER_ENTERS_ALTERNATIVE_TEXT, CONTINUE, CUSTOMER_ENTERS_NUMBER); }
/** * Starts the flow as an alternative to the specified step, in this flow's use * case. * * @param stepName * the name of the specified step * @return the flow position part, to ease creation of the condition and the * first step of the flow * @throws NoSuchElementInModel * if the specified step is not found in this * flow's use case */ public FlowPositionPart insteadOf(String stepName) { FlowStep step = (FlowStep) useCase.findStep(stepName); InsteadOf insteadOf = new InsteadOf(step); optionalFlowPositionPart = new FlowPositionPart(insteadOf, this); return optionalFlowPositionPart; }
@Test public void sameEventTypeReactedTo() { Model model = modelBuilder.useCase(USE_CASE) .basicFlow().anytime() .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) .flow("Alternative Flow: Could react as well").anytime() .step(CUSTOMER_ENTERS_ALTERNATIVE_TEXT).user(EntersText.class).system(displaysEnteredText()) .build(); modelRunner.run(model); Set<Class<?>> reactToTypes = modelRunner.getReactToTypes(); assertEquals(1, reactToTypes.size()); Class<?> eventTypeReactedTo = reactToTypes.iterator().next(); assertEquals(EntersText.class, eventTypeReactedTo); }
.step("S6").system(quits()) .flow("Alternative flow A").insteadOf("S4") .step("S4a_1").system(blowsUp()) .step("S4a_2").continuesAt("S1") .flow("Alternative flow B").after("S3") .step("S4b_1").continuesAfter("S2") .flow("Alternative flow C").condition(thereIsNoAlternative()) .step("S5a").continuesWithoutAlternativeAt("S4") .flow("Alternative flow D").insteadOf("S4").condition(thereIsNoAlternative()) .step("S4c_1").includesUseCase("Included use case") .step("S4c_2").continuesAt("S1") .flow("EX").anytime() .step("EX1").on(Exception.class).system(logsException()) .build();
@Test public void continuesWithoutAlternativeAtCalledFromMultipleMutuallyExclusiveAlternativeFlows() { Model model = modelBuilder .useCase(USE_CASE) .basicFlow() .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) .step(CUSTOMER_ENTERS_TEXT_AGAIN).user(EntersText.class).system(displaysEnteredText()) .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) .flow(ALTERNATIVE_FLOW).insteadOf(CUSTOMER_ENTERS_TEXT_AGAIN).condition(this::textIsAvailable) .step(CUSTOMER_ENTERS_ALTERNATIVE_TEXT).user(EntersText.class).system(displaysEnteredText()) .step(CONTINUE).continuesWithoutAlternativeAt(CUSTOMER_ENTERS_TEXT_AGAIN) .flow(ALTERNATIVE_FLOW_2).insteadOf(CUSTOMER_ENTERS_TEXT_AGAIN).condition(this::textIsNotAvailable) .step(CUSTOMER_ENTERS_NUMBER_AGAIN).user(EntersNumber.class).system(displaysEnteredNumber()) .step(CONTINUE_2).continuesWithoutAlternativeAt(CUSTOMER_ENTERS_TEXT_AGAIN) .build(); modelRunner.run(model).reactTo(entersText(), entersAlternativeText(), entersText(), entersNumber()); assertRecordedStepNames(CUSTOMER_ENTERS_TEXT, CUSTOMER_ENTERS_ALTERNATIVE_TEXT, CONTINUE, CUSTOMER_ENTERS_TEXT_AGAIN, CUSTOMER_ENTERS_NUMBER); } }
/** * Starts the flow after the specified step has been run, in this flow's use * case. * * Note: You should use after to handle exceptions that occurred in the * specified step. * * @param stepName * the name of the step to start the flow after * @return the flow position part, to ease creation of the condition and the * first step of the flow * @throws NoSuchElementInModel * if the specified step is not found in a flow * of this use case * */ public FlowPositionPart after(String stepName) { Step step = useCase.findStep(stepName); After after = new After((FlowStep) step); optionalFlowPositionPart = new FlowPositionPart(after, this); return optionalFlowPositionPart; }
@Test public void differentEventTypesReactedTo() { Model model = modelBuilder.useCase(USE_CASE) .basicFlow().anytime() .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) .flow("Alternative Flow: Could react as well").anytime() .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) .build(); modelRunner.run(model); Set<Class<?>> reactToTypes = modelRunner.getReactToTypes(); assertEquals(2, reactToTypes.size()); assertTrue(reactToTypes.contains(EntersText.class)); assertTrue(reactToTypes.contains(EntersNumber.class)); }
@Test public void moreThanOneStepCanReact() { Model model = modelBuilder.useCase(USE_CASE) .basicFlow().anytime() .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) .flow("Alternative Flow: Could react as well").anytime() .step(CUSTOMER_ENTERS_ALTERNATIVE_TEXT).user(EntersText.class).system(displaysEnteredText()) .build(); modelRunner.run(model); boolean canReact = modelRunner.canReactTo(entersText().getClass()); assertTrue(canReact); Set<Step> stepsThatCanReact = modelRunner.getStepsThatCanReactTo(entersText().getClass()); assertEquals(2, stepsThatCanReact.size()); } }
@Test public void eventTypesReactedOnlyIfFlowPositionIsRight() { Model model = modelBuilder.useCase(USE_CASE) .basicFlow() .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) .flow("Alternative Flow: Could react as well").after(CUSTOMER_ENTERS_TEXT) .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) .build(); modelRunner.run(model); Set<Class<?>> reactToTypes = modelRunner.getReactToTypes(); assertEquals(1, reactToTypes.size()); Class<?> eventTypeReactedTo = reactToTypes.iterator().next(); assertEquals(EntersText.class, eventTypeReactedTo); modelRunner.reactTo(entersText()); reactToTypes = modelRunner.getReactToTypes(); assertEquals(1, reactToTypes.size()); eventTypeReactedTo = reactToTypes.iterator().next(); assertEquals(EntersNumber.class, eventTypeReactedTo); } }
@Test public void throwsExceptionIfMoreThanOneStepCanReactInSameUseCase() { thrown.expect(MoreThanOneStepCanReact.class); thrown.expectMessage(CUSTOMER_ENTERS_TEXT); thrown.expectMessage(CUSTOMER_ENTERS_ALTERNATIVE_TEXT); Model model = modelBuilder .useCase(USE_CASE) .basicFlow().anytime() .step(CUSTOMER_ENTERS_TEXT).system(displaysConstantText()) .flow(ALTERNATIVE_FLOW).anytime() .step(CUSTOMER_ENTERS_ALTERNATIVE_TEXT).system(displaysConstantText()) .build(); modelRunner.run(model); }
@Test public void eventTypesReactedOnlyIfConditionFulfilled() { Model model = modelBuilder.useCase(USE_CASE) .basicFlow().condition(() -> false) .step(CUSTOMER_ENTERS_TEXT).user(EntersText.class).system(displaysEnteredText()) .flow("Alternative Flow: Could react as well").anytime() .step(CUSTOMER_ENTERS_NUMBER).user(EntersNumber.class).system(displaysEnteredNumber()) .build(); modelRunner.run(model); Set<Class<?>> reactToTypes = modelRunner.getReactToTypes(); assertEquals(1, reactToTypes.size()); Class<?> eventTypeReactedTo = reactToTypes.iterator().next(); assertEquals(EntersNumber.class, eventTypeReactedTo); }
@Test public void handlesExceptionAtAnyTime() { Model model = modelBuilder.useCase(USE_CASE) .basicFlow() .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) .flow(ALTERNATIVE_FLOW).after(SYSTEM_DISPLAYS_TEXT) .step(SYSTEM_THROWS_EXCEPTION).system(throwsArrayIndexOutOfBoundsException()) .flow(ALTERNATIVE_FLOW_2).anytime() .step(SYSTEM_HANDLES_EXCEPTION).on(ArrayIndexOutOfBoundsException.class).system(e -> {}) .build(); modelRunner.run(model); assertRecordedStepNames(SYSTEM_DISPLAYS_TEXT, SYSTEM_THROWS_EXCEPTION, SYSTEM_HANDLES_EXCEPTION); } }
@Test public void doesNotHandleExceptionIfNoExceptionOccurs() { Model model = modelBuilder.useCase(USE_CASE) .basicFlow() .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) .flow(ALTERNATIVE_FLOW).after(SYSTEM_DISPLAYS_TEXT) .step(SYSTEM_HANDLES_EXCEPTION).on(ArrayIndexOutOfBoundsException.class).system(e -> {}) .build(); modelRunner.run(model); assertRecordedStepNames(SYSTEM_DISPLAYS_TEXT); }
@Test public void doesNotHandleExceptionIfSystemReactionDoesNotThrowException() { Model model = modelBuilder.useCase(USE_CASE) .basicFlow() .step(SYSTEM_DISPLAYS_TEXT).system(displaysConstantText()) .flow(ALTERNATIVE_FLOW).after(SYSTEM_DISPLAYS_TEXT) .step(SYSTEM_HANDLES_EXCEPTION).on(ArrayIndexOutOfBoundsException.class).system(e -> {}) .build(); modelRunner.run(model); assertRecordedStepNames(SYSTEM_DISPLAYS_TEXT); }