Testing 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 class BaseTestFlow  extends AbstractXmlFlowExecutionTests {
      protected ApplicationContext applicationContext;

      /**
       * This method returns the flow to be tested.
       */
      protected FlowDefinitionResource getResource(FlowDefinitionResourceFactory resourceFactory) {
            return resourceFactory.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.
       */
      protected FlowDefinitionResource[]
       getModelResources(FlowDefinitionResourceFactory resourceFactory) {
        FlowDefinitionResource[] flowDefinitionResources = new FlowDefinitionResource[1];
        flowDefinitionResources[0] = resourceFactory.createFileResource("src/test/resources/flows/parent.xml");
        return flowDefinitionResources;
      }

      /**
       * Registers beans used by the tested flow.
       */
      protected void configureFlowBuilderContext(MockFlowBuilderContext builderContext) {
            applicationContext = new ClassPathXmlApplicationContext(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 void testFlowStarts() {
            MockExternalContext externalContext = new MockExternalContext();
            startFlow(externalContext);
            assertFlowExecutionActive();
            assertCurrentStateEquals("startPage");
      }

      public void testNavigation() {
            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 void testGlobalTransition() {
            MockExternalContext externalContext = new MockExternalContext();
            setCurrentState("startPage");
            externalContext.setEventId("next");
            externalContext.getMockRequestParameterMap().put("inputField", "error");
            resumeFlow(externalContext);
            assertFlowExecutionOutcomeEquals("errorState");
      }
}

Labels: , ,