AbstractFlowExecutionTests.java

/*
 * 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
 *
 *      https://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
 * 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();
}