Chariot Training Classes

Training Courses

I run Chariot's training and mentoring services. We provide training in AngularJS, HTML5, Spring, Hibernate, Maven, Scala, and more.

Chariot Education Services

Technology

Chariot Emerging Tech

Learn about upcoming technologies and trends from my colleagues at Chariot Solutions.

Resources

Chariot Conferences

Podcasts

« Quizzo in Roo case of the Mondays. Or, how you can be a JPA doofus | Main | TGIF - Ok, last Friday Post - Spring Data JPA Makes it Sweeter »
Saturday
Feb252012

Quizzo Saturday - testing web flows

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 startFlow method, which launches the flow.
  • Checks to make sure that the flow is waiting on the register-team view state.
  • Checks to make sure the Web Flow engine creates the variable, teamSetupForm in the var webflow 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.

PrintView Printer Friendly Version

EmailEmail Article to Friend

Reader Comments

There are no comments for this journal entry. To create a new comment, use the form below.

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>