Testing Spring Webflow 2 with inheritance

This blog entry shows how to test a flow with inheritance in Spring Webflow 2. The flow to be tested consists of a simple navigation which starts with a view state and ends getting to another view state that will depend on the result of the execution of a controller. This flow extends another flow which basically contains a redirection to a common page in case of error.

Introduction

The test flow (main.xml) is as follows:
<?xml version="1.0"encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"
      parent="parent">

      <view-state id="startPage"view="main/startPage.jsp">
            <transition on="next"to="generateError"/>
      </view-state>
      
      <action-state id="generateError">
            <evaluate expression="checkParameterController"/>
            <transition on="ok"to="OkView"/>
            <transition on="ko"to="KoView"/>
      </action-state>


      <end-state id="OkView"view="main/finalOkView.jsp"/>
      <end-state id="KoView"view="main/finalKoView.jsp"/>
</flow>

 

And the parent flow (parent.xml):

<?xml version="1.0"encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd"
      abstract="true">


      <end-state id="errorState"view="commonErrorPage.jsp"/>


      <global-transitions>
            <transition on-exception="java.lang.Exception"to="errorState"/>
      </global-transitions>
</flow>

 

When executing tests, Spring provides us with a quite useful class: AbstractXmlFlowExecutionTests, in the org.springframework.webflow.test.execution package. This class has a variety of methods that will help us test the flow. The most interesting:
FlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory)

 

Here, we specify where the flow which we want to test is located.

FlowDefinitionResource[] getModelResources(FlowDefinitionResourceFactory resourceFactory)

 

If the flow uses inheritance, we define the parent flow here.

void configureFlowBuilderContext(MockFlowBuilderContext builderContext)

Lets us customize the builder context. I use it for registering the beans that will use in the test as this is the way it’s indicated at the class javadoc.

void registerMockFlowBeans(ConfigurableBeanFactory flowBeanFactory)

Allows registering the beans that will be used by the flow to be tested. For example:

flowBeanFactory.registerSingleton("myService", new MyMockService());

 

Testing the flow

I’ve divided it in two classes in order to separate configuration from tests cases:

public classBaseTestFlow  extendsAbstractXmlFlowExecutionTests {
      protected ApplicationContext applicationContext;


      /**
       * This method returns the flow to be tested.
       */
      protectedFlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory) {
            returnresourceFactory.createFileResource("src/test/resources/flows/main.xml");
      }


      /**
       * Needs to be overridden if the flow to be tested inherits from another flow. In this case, we register its parent flow.
       */
      protectedFlowDefinitionResource[]
       getModelResources(FlowDefinitionResourceFactory resourceFactory) {
        FlowDefinitionResource[] flowDefinitionResources = new FlowDefinitionResource[1];
        flowDefinitionResources[0] = resourceFactory.createFileResource("src/test/resources/flows/parent.xml");
        returnflowDefinitionResources;
      }


      /**
       * Registers beans used by the tested flow.
       */
      protected voidconfigureFlowBuilderContext(MockFlowBuilderContext builderContext) {
            applicationContext = newClassPathXmlApplicationContext(new String[] {
                  "classpath:xpadro/spring/test/configuration/root-context.xml",
                  "classpath:xpadro/spring/test/configuration/app-context.xml"
            });
            builderContext.registerBean("checkParameterController", applicationContext.getBean("checkParameterController"));
      }


      /*
      protected void registerMockFlowBeans(ConfigurableBeanFactory flowBeanFactory) {
            flowBeanFactory.registerSingleton("checkParameterController", new CheckParameterController());
      }
      */
}

 

You could simply delete configureFlowBuilderContext method and use registerMockFlowBeans method instead if you don’t want/need to start your own test context.

public class TestFlow extends BaseTestFlow {
      public voidtestFlowStarts() {
            MockExternalContext externalContext = new MockExternalContext();
            startFlow(externalContext);
            assertFlowExecutionActive();
            assertCurrentStateEquals("startPage");
      }


      public voidtestNavigation() {
            MockExternalContext externalContext = new MockExternalContext();
            setCurrentState("startPage");
            externalContext.setEventId("next");
            externalContext.getMockRequestParameterMap().put("inputField", "yes");
            getFlowScope().put("testFlowVar", "testValue");
            resumeFlow(externalContext);
            assertCurrentStateEquals("OkView");
            assertEquals("testValue", getFlowScope().get("testFlowVar"));
      }


      public voidtestGlobalTransition() {
            MockExternalContext externalContext = new MockExternalContext();
            setCurrentState("startPage");
            externalContext.setEventId("next");
            externalContext.getMockRequestParameterMap().put("inputField", "error");
            resumeFlow(externalContext);
            assertFlowExecutionOutcomeEquals("errorState");
      }
}