Quizzo Saturday - testing web flows
Saturday, February 25, 2012 at 11:54AM GitHub commit URLs: 82add3e1e23eee8b84058c12fcd6ff211f70096e and more importantly, 4cfed580c3404e61d501ac4ee0182c294b1b0b89
Ok, this is kind of a tangent. But I wanted to really debug the flow logic for these interactions, and better understand the flow logic itself before I bother to write up nice looking views.
Test that our flow starts with the correct state
I started with this first step, which is pretty easy to test:
<var class="com.chariot.games.quizzo.web.flow.TeamSetupForm" name="teamSetupForm"/>
<view-state id="register-team" view="playQuizzo/register-team"
model="flowScope.teamSetupForm">
<transition on="continue"
to="register-team-members"
bind="true" validate="true"/>
</view-state>
This state fills in a portion of the teamSetupForm flow-scoped variable. How do we test this? Somewhat easily. First, we start by building a test that is a subclass of AbstractXmlFlowExecutionTests:
public class QuizzoFlowTest extends AbstractXmlFlowExecutionTests {
@Override
protected FlowDefinitionResource getResource(
FlowDefinitionResourceFactory resourceFactory) {
return resourceFactory.
createFileResource("src/main/webapp/WEB-INF/views/playQuizzo/flow.xml");
}
}
Next, we have to build a method to expose a stub version of our Web Flow Spring Bean, the QuizzoFlowManager. It's referred to in the rest of the flow as quizzoFlowManager. This stub will replace our Spring Bean, and will provide access to any methods the flow calls. The flow execution engine may throw errors if it looks for this bean while parsing the form. So, we add this method to the test:
@Override
protected void configureFlowBuilderContext(
MockFlowBuilderContext builderContext) {
StubQuizzoFlowManager stubQuizzoFlowManager =
new StubQuizzoFlowManager();
builderContext.registerBean("quizzoFlowManager",
stubQuizzoFlowManager);
}
This method is called once we execute any flow start or resume methods in our mock flow engine.
Writing a flow test
Consider the test code below:
public void testStartFlow() {
MutableAttributeMap map = new LocalAttributeMap();
MockExternalContext context = new MockExternalContext();
startFlow(map, context);
assertCurrentStateEquals("register-team");
TeamSetupForm form = (TeamSetupForm) getFlowScope().get("teamSetupForm");
assertNotNull(form);
}
The test does the following things:
- Sets up a attribute map, required for starting a flow. This is just a property map.
- Builds a fake Flow context, also required when launching a flow.
- Calls the base test class's
startFlowmethod, which launches the flow. - Checks to make sure that the flow is waiting on the
register-teamview state. - Checks to make sure the Web Flow engine creates the variable,
teamSetupFormin thevarwebflow tag. - Checks to make sure the form is not null.
But what about checking events? For that, we'll push a "continue" event, and set the values that the form would have submitted in our Form bean:
@Test
public void testSubmitTeamName() {
setCurrentState("register-team");
MockExternalContext context = new MockExternalContext();
getFlowScope().put("teamSetupForm",
createTeamSetupForm("The Jets", "When you're a Jet you're a Jet"));
context.setEventId("continue");
resumeFlow(context);
assertCurrentStateEquals("register-team-members");
}
In this test, we start with the flow sitting on the register-team state. Since Web Flow is not running any states before our unit test, we have to create our teamSetupForm ourselves, and set the value of it using a helper method to include the team name and team message.
We then tell the context to send the continue event, and resume our flow. We check once this completes to make sure we are now sitting on the register-team-members state.
A more complex example
Now we are going to test the next state and its transition to the following state using continue:
<view-state id="register-team-members" view="playQuizzo/register-team-members"
model="flowScope.teamSetupForm">
<transition on="continue" to="ready-to-play" bind="true" validate="true">
<evaluate expression="quizzoFlowManager.saveTeamData(flowRequestContext)"/>
</transition>
<transition on="add-team-member"/>
<transition on="remove-team-member"/>
<transition on="back" to="register-team"/>
</view-state>
<action-state id="ready-to-play">
<evaluate expression="quizzoFlowManager.pollReady(flowRequestContext)"/>
<transition on="yes" to="play-round"/>
<transition on="poll" to="poll"/>
</action-state>
You may think this test executes just fine:
@Test
public void testSubmitTeamMembers() {
setCurrentState("register-team-members");
MockExternalContext context = new MockExternalContext();
getFlowScope().put("teamSetupForm",
createTeamSetupForm("The Jets",
"When you're a Jet you're a Jet",
"Ice", "Action", "Baby John", "Tiger", "Joyboy"));
context.setEventId("continue");
resumeFlow(context);
assertCurrentStateEquals("ready-to-play");
}
But it fails on the state assertion. It states that the state is actually play-round. How can this be?
Action States
This is because we're using an action state from our web flow. Action states do not wait on anything. They immediately execute their evaluations, and take action from their responses. I knew this when building the flow, but scanned by it when writing the test. Ugh.
How did I figure this out? You'd think by closely reading the ready-to-play state definition, but no... Instead, I amped up the logging for the webflow framework by setting this entry in log4j.properties:
log4j.logger.org.springframework.webflow=trace
This rewarded me with a great amount of useful detail:
2012-02-25 12:19:47,032 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImplFactory - Creating new execution of 'flow' 2012-02-25 12:19:47,047 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImpl - Resuming in org.springframework.webflow.test.MockExternalContext@1494cb8b 2012-02-25 12:19:47,054 [main] DEBUG org.springframework.webflow.engine.Flow - Restoring [FlowVariable@3209fa8f name = 'teamSetupForm', valueFactory = [BeanFactoryVariableValueFactory@2d20dbf3 type = TeamSetupForm]] 2012-02-25 12:19:47,072 [main] DEBUG org.springframework.webflow.engine.ViewState - Event 'continue' returned from view [MockViewFactoryCreator.MockView@3e0d1329 viewId = 'playQuizzo/register-team-members'] 2012-02-25 12:19:47,074 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@2326a29c expression = quizzoFlowManager.saveTeamData(flowRequestContext), resultExpression = [null]] 2012-02-25 12:19:47,074 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]] ... 2012-02-25 12:19:47,084 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@2326a29c expression = quizzoFlowManager.saveTeamData(flowRequestContext), resultExpression = [null]]; result = success 2012-02-25 12:19:47,085 [main] DEBUG org.springframework.webflow.engine.Transition - Executing [Transition@3a4c5b4 on = continue, to = ready-to-play] 2012-02-25 12:19:47,085 [main] DEBUG org.springframework.webflow.engine.Transition - Exiting state 'register-team-members' 2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.engine.ActionState - Entering state 'ready-to-play' of flow 'flow' 2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@36afae4a expression = quizzoFlowManager.pollReady(flowRequestContext), resultExpression = [null]] 2012-02-25 12:19:47,086 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]] 2012-02-25 12:19:47,087 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Clearing action execution attributes map[[empty]] 2012-02-25 12:19:47,087 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@36afae4a expression = quizzoFlowManager.pollReady(flowRequestContext), resultExpression = [null]]; result = yes 2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.Transition - Executing [Transition@47db9852 on = yes, to = play-round] 2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.Transition - Exiting state 'ready-to-play' 2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.engine.ViewState - Entering state 'play-round' of flow 'flow' 2012-02-25 12:19:47,088 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Executing [EvaluateAction@21ed5459 expression = quizzoFlowManager.setupQuestionAndChoices(flowRequestContext), resultExpression = [null]] 2012-02-25 12:19:47,089 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Putting action execution attributes map[[empty]] 2012-02-25 12:19:47,090 [main] DEBUG org.springframework.webflow.execution.AnnotatedAction - Clearing action execution attributes map[[empty]] 2012-02-25 12:19:47,090 [main] DEBUG org.springframework.webflow.execution.ActionExecutor - Finished executing [EvaluateAction@21ed5459 expression = quizzoFlowManager.setupQuestionAndChoices(flowRequestContext), resultExpression = [null]]; result = success 2012-02-25 12:19:47,091 [main] DEBUG org.springframework.webflow.engine.impl.FlowExecutionImpl - Assigned key 1 2012-02-25 12:19:47,091 [main] DEBUG org.springframework.webflow.engine.ViewState - Rendering + [MockViewFactoryCreator.MockView@643cb075 viewId = 'playQuizzo/play-round'] 2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.ViewState - Flash scope = map[[empty]] 2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.ViewState - Messages = [DefaultMessageContext@4c6504bc sourceMessages = map[[null] -> list[[empty]]]] 2012-02-25 12:19:47,092 [main] DEBUG org.springframework.webflow.engine.Transition - Completed transition execution. As a result, the new state is 'play-round' in flow 'flow' 2012-02-25 12:19:47,093 [main] DEBUG org.springframework.webflow.engine.Transition - Completed transition execution. As a result, the new state is 'play-round' in flow 'flow'
If you spend time reading this, you'll see two flow state executions - one for the view state, and one for the action state. Our stub class returns "yes" from our transition in the poller state, and so we move to the next view state, play-round
Wrap-up
Testing in WebFlow is rather difficult, because of the number of mock objects, the complexity of flow logic, and the fact that it's in XML, which is not compiled and therefore really requires tests. But is is worth it, as long as you're not using this as an excuse to test your beans themselves. You should test those either in unit or Spring integration tests. My assertions are mostly around making sure we submitted the right events to trigger the right view states.
Next I'll be wiring up some pages to these flow view states, and starting to really get the processing going. I'm also going to start writing an admin page to control the game itself and provide an audience view.
JUnit,
quizzo-in-roo,
spring-roo,
webflow 


Reader Comments