/* * Copyright 2004-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.webflow.test.execution; import junit.framework.TestCase; import org.springframework.util.Assert; import org.springframework.webflow.context.ExternalContext; import org.springframework.webflow.core.collection.MutableAttributeMap; import org.springframework.webflow.definition.FlowDefinition; import org.springframework.webflow.engine.impl.FlowExecutionImpl; import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; import org.springframework.webflow.execution.FlowExecution; import org.springframework.webflow.execution.FlowExecutionException; import org.springframework.webflow.execution.FlowExecutionFactory; import org.springframework.webflow.execution.FlowExecutionOutcome; import org.springframework.webflow.test.MockExternalContext; /** * Base class for tests that verify a flow executes as expected. Flow execution tests authored by subclasses should test * that a flow responds to all supported transition criteria correctly, transitioning to the correct states and * producing the expected results on the occurrence of external events. * <p> * A typical flow execution test case will test: * <ul> * <li>That the flow execution starts as expected (see {@link #startFlow(MutableAttributeMap, ExternalContext)}). * <li>That a state executes the appropriate transition when an event is signaled. A test case should be authored for * each logical event that can occur, where an event triggers a transition representing a path through the flow. The * goal should be to exercise all state transitions (see the {@link #resumeFlow(ExternalContext)} variants and the * {@link #setCurrentState(String)} for more information). * <li>That given a transition that leads to an interactive state type (such as a view state or an end state), the view * selected matches what was expected and the current state of the flow matches what is expected. * </ul> * <p> * A flow execution test can effectively automate and validate the orchestration required to drive an end-to-end * business task that spans several steps involving the user to complete. Such tests are a good way to test your system * top-down starting at the web-tier and pushing through all the way to the DB without having to deploy to a servlet or * portlet container. In addition, they can be used to effectively test a flow's execution (the web layer) standalone, * typically with a mock service layer. * * @author Keith Donald */ public abstract class AbstractFlowExecutionTests extends TestCase { /** * The factory that will create the flow execution to test. */ private FlowExecutionFactory flowExecutionFactory; /** * The flow execution running the flow when the test is active (runtime object). */ private FlowExecution flowExecution; /** * The outcome that was reached when the flow ends; initially null. */ private FlowExecutionOutcome flowExecutionOutcome; /** * Constructs a default flow execution test. * @see #setName(String) */ public AbstractFlowExecutionTests() { super(); } /** * Constructs a flow execution test with given name. * @param name the name of the test */ public AbstractFlowExecutionTests(String name) { super(name); } /** * Gets the factory that will create the flow execution to test. This method will create the factory if it is not * already set. * @return the flow execution factory * @see #createFlowExecutionFactory() */ protected FlowExecutionFactory getFlowExecutionFactory() { if (flowExecutionFactory == null) { flowExecutionFactory = createFlowExecutionFactory(); } return flowExecutionFactory; } /** * Start the flow execution to be tested. * @param context the external context providing information about the caller's environment, used by the flow * execution during the start operation * @throws FlowExecutionException if an exception was thrown while starting the flow execution */ protected void startFlow(ExternalContext context) throws FlowExecutionException { startFlow(null, context); } /** * Start the flow execution to be tested. * @param input input to pass the flow * @param context the external context providing information about the caller's environment, used by the flow * execution during the start operation * @throws FlowExecutionException if an exception was thrown while starting the flow execution */ protected void startFlow(MutableAttributeMap<?> input, ExternalContext context) throws FlowExecutionException { flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition()); flowExecution.start(input, context); if (flowExecution.hasEnded()) { flowExecutionOutcome = flowExecution.getOutcome(); } } /** * Resume the flow execution to be tested. * @param context the external context providing information about the caller's environment, used by the flow * execution during the start operation * @throws FlowExecutionException if an exception was thrown while starting the flow execution */ protected void resumeFlow(ExternalContext context) throws FlowExecutionException { Assert.state(flowExecution != null, "The flow execution to test is [null]; " + "you must start the flow execution before you can resume it!"); flowExecution.resume(context); if (flowExecution.hasEnded()) { flowExecutionOutcome = flowExecution.getOutcome(); } } /** * Sets the current state of the flow execution being tested. If the execution has not been started, it will be * created and activated. * @param stateId the state id */ protected void setCurrentState(String stateId) { if (flowExecution == null) { flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition()); } ((FlowExecutionImpl) flowExecution).setCurrentState(stateId); } // convenience accessors /** * Returns the flow execution being tested. * @return the flow execution * @throws IllegalStateException the execution has not been started */ protected FlowExecution getFlowExecution() throws IllegalStateException { return flowExecution; } /** * Returns the flow execution outcome that was reached. * @return the flow execution outcome, or null if the flow execution has not ended */ protected FlowExecutionOutcome getFlowExecutionOutcome() { return flowExecutionOutcome; } /** * Returns view scope. * @return view scope */ protected MutableAttributeMap<Object> getViewScope() throws IllegalStateException { return getFlowExecution().getActiveSession().getViewScope(); } /** * Returns flow scope. * @return flow scope */ protected MutableAttributeMap<Object> getFlowScope() throws IllegalStateException { return getFlowExecution().getActiveSession().getScope(); } /** * Returns conversation scope. * @return conversation scope */ protected MutableAttributeMap<Object> getConversationScope() throws IllegalStateException { return getFlowExecution().getConversationScope(); } /** * Returns the attribute in view scope. View-scoped attributes are local to the current view state and are cleared * when the view state exits. * @param attributeName the name of the attribute * @return the attribute value */ protected Object getViewAttribute(String attributeName) { return getFlowExecution().getActiveSession().getViewScope().get(attributeName); } /** * Returns the required attribute in view scope; asserts the attribute is present. View-scoped attributes are local * to the current view state and are cleared when the view state exits. * @param attributeName the name of the attribute * @return the attribute value * @throws IllegalStateException if the attribute was not present */ protected Object getRequiredViewAttribute(String attributeName) throws IllegalStateException { return getFlowExecution().getActiveSession().getViewScope().getRequired(attributeName); } /** * Returns the required attribute in view scope; asserts the attribute is present and of the correct type. * View-scoped attributes are local to the current view state and are cleared when the view state exits. * @param attributeName the name of the attribute * @return the attribute value * @throws IllegalStateException if the attribute was not present or was of the wrong type */ protected Object getRequiredViewAttribute(String attributeName, Class<Object> requiredType) throws IllegalStateException { return getFlowExecution().getActiveSession().getViewScope().getRequired(attributeName, requiredType); } /** * Returns the attribute in flow scope. Flow-scoped attributes are local to the active flow session. * @param attributeName the name of the attribute * @return the attribute value */ protected Object getFlowAttribute(String attributeName) { return getFlowExecution().getActiveSession().getScope().get(attributeName); } /** * Returns the required attribute in flow scope; asserts the attribute is present. Flow-scoped attributes are local * to the active flow session. * @param attributeName the name of the attribute * @return the attribute value * @throws IllegalStateException if the attribute was not present */ protected Object getRequiredFlowAttribute(String attributeName) throws IllegalStateException { return getFlowExecution().getActiveSession().getScope().getRequired(attributeName); } /** * Returns the required attribute in flow scope; asserts the attribute is present and of the correct type. * Flow-scoped attributes are local to the active flow session. * @param attributeName the name of the attribute * @return the attribute value * @throws IllegalStateException if the attribute was not present or was of the wrong type */ protected Object getRequiredFlowAttribute(String attributeName, Class<Object> requiredType) throws IllegalStateException { return getFlowExecution().getActiveSession().getScope().getRequired(attributeName, requiredType); } /** * Returns the attribute in conversation scope. Conversation-scoped attributes are shared by all flow sessions. * @param attributeName the name of the attribute * @return the attribute value */ protected Object getConversationAttribute(String attributeName) { return getFlowExecution().getConversationScope().get(attributeName); } /** * Returns the required attribute in conversation scope; asserts the attribute is present. Conversation-scoped * attributes are shared by all flow sessions. * @param attributeName the name of the attribute * @return the attribute value * @throws IllegalStateException if the attribute was not present */ protected Object getRequiredConversationAttribute(String attributeName) throws IllegalStateException { return getFlowExecution().getConversationScope().getRequired(attributeName); } /** * Returns the required attribute in conversation scope; asserts the attribute is present and of the required type. * Conversation-scoped attributes are shared by all flow sessions. * @param attributeName the name of the attribute * @return the attribute value * @throws IllegalStateException if the attribute was not present or not of the required type */ protected Object getRequiredConversationAttribute(String attributeName, Class<?> requiredType) throws IllegalStateException { return getFlowExecution().getConversationScope().getRequired(attributeName, requiredType); } // assert helpers /** * Assert that the entire flow execution is active; that is, it has not ended and has been started. */ protected void assertFlowExecutionActive() { assertTrue("The flow execution is not active but it should be", getFlowExecution().isActive()); } /** * Assert that the active flow session is for the flow with the provided id. * @param expectedActiveFlowId the flow id that should have a session active in the tested flow execution */ protected void assertActiveFlowEquals(String expectedActiveFlowId) { assertEquals("The active flow id '" + getFlowExecution().getActiveSession().getDefinition().getId() + "' does not equal the expected active flow id '" + expectedActiveFlowId + "'", expectedActiveFlowId, getFlowExecution().getActiveSession().getDefinition().getId()); } /** * Assert that the flow execution has ended; that is, it is no longer active. */ protected void assertFlowExecutionEnded() { assertTrue("The flow execution is still active but it should have ended", getFlowExecution().hasEnded()); } /** * Assert that the flow execution has ended with the outcome specified. * @param outcome the name of the flow execution outcome */ protected void assertFlowExecutionOutcomeEquals(String outcome) { assertNotNull("There has been no flow execution outcome", flowExecutionOutcome); assertEquals("The flow execution outcome is wrong", outcome, flowExecutionOutcome.getId()); } /** * Assert that the current state of the flow execution equals the provided state id. * @param expectedCurrentStateId the expected current state */ protected void assertCurrentStateEquals(String expectedCurrentStateId) { assertEquals("The current state '" + getFlowExecution().getActiveSession().getState().getId() + "' does not equal the expected state '" + expectedCurrentStateId + "'", expectedCurrentStateId, getFlowExecution().getActiveSession().getState().getId()); } /** * Assert that the response written to the mock context equals the response provided. * @param response the expected response * @param context the mock external context that was written to */ protected void assertResponseWrittenEquals(String response, MockExternalContext context) { assertEquals(response, context.getMockResponseWriter().getBuffer().toString()); } /** * Factory method to create the flow execution factory. Subclasses could override this if they want to use a custom * flow execution factory or custom configuration of the flow execution factory, registering flow execution * listeners for instance. The default implementation just returns a {@link FlowExecutionImplFactory} instance. * @return the flow execution factory */ protected FlowExecutionFactory createFlowExecutionFactory() { return new FlowExecutionImplFactory(); } /** * Directly update the flow execution used by the test by setting it to given flow execution. Use this if you have * somehow manipulated the flow execution being tested and want to continue the test with another flow execution. * @param flowExecution the flow execution to use */ protected void updateFlowExecution(FlowExecution flowExecution) { this.flowExecution = flowExecution; } /** * Returns the flow definition to be tested. Subclasses must implement. * @return the flow definition */ protected abstract FlowDefinition getFlowDefinition(); }