KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > springframework > webflow > test > execution > AbstractFlowExecutionTests


1 /*
2  * Copyright 2002-2006 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16 package org.springframework.webflow.test.execution;
17
18 import java.util.Collection JavaDoc;
19 import java.util.Map JavaDoc;
20
21 import junit.framework.TestCase;
22
23 import org.springframework.binding.expression.ExpressionParser;
24 import org.springframework.core.style.StylerUtils;
25 import org.springframework.util.Assert;
26 import org.springframework.webflow.context.ExternalContext;
27 import org.springframework.webflow.core.DefaultExpressionParserFactory;
28 import org.springframework.webflow.core.collection.MutableAttributeMap;
29 import org.springframework.webflow.core.collection.ParameterMap;
30 import org.springframework.webflow.definition.FlowDefinition;
31 import org.springframework.webflow.engine.impl.FlowExecutionImplFactory;
32 import org.springframework.webflow.execution.FlowExecution;
33 import org.springframework.webflow.execution.FlowExecutionException;
34 import org.springframework.webflow.execution.FlowExecutionFactory;
35 import org.springframework.webflow.execution.ViewSelection;
36 import org.springframework.webflow.execution.support.ApplicationView;
37 import org.springframework.webflow.execution.support.ExternalRedirect;
38 import org.springframework.webflow.execution.support.FlowDefinitionRedirect;
39 import org.springframework.webflow.execution.support.FlowExecutionRedirect;
40 import org.springframework.webflow.test.MockExternalContext;
41
42 /**
43  * Base class for integration tests that verify a flow executes as expected.
44  * Flow execution tests captured by subclasses should test that a flow responds
45  * to all supported transition criteria correctly, transitioning to the correct
46  * states and producing the expected results on the occurence of possible
47  * external (user) events.
48  * <p>
49  * More specifically, a typical flow execution test case will test:
50  * <ul>
51  * <li>That the flow execution starts as expected given a request from an
52  * external context containing potential input attributes (see the
53  * {@link #startFlow(MutableAttributeMap, ExternalContext)} variants).
54  * <li>That given the set of supported state transition criteria a state
55  * executes the appropriate transition when a matching event is signaled (with
56  * potential input request parameters, see the
57  * {@link #signalEvent(String, ExternalContext)} variants). A test case should
58  * be coded for each logical event that can occur, where an event drives a
59  * possible path through the flow. The goal should be to exercise all possible
60  * paths of the flow. Use a test coverage tool like Clover or Emma to assist
61  * with measuring your test's effectiveness.
62  * <li>That given a transition that leads to an interactive state type (a view
63  * state or an end state) that the view selection returned to the client matches
64  * what was expected and the current state of the flow matches what is expected.
65  * </ul>
66  * <p>
67  * A flow execution test can effectively automate and validate the orchestration
68  * required to drive an end-to-end business task that spans several steps
69  * involving the user to complete. Such tests are a good way to test your system
70  * top-down starting at the web-tier and pushing through all the way to the DB
71  * without having to deploy to a servlet or portlet container. In addition, they
72  * can be used to effectively test a flow's execution (the web layer)
73  * standalone, typically with a mock service layer. Both styles of testing are
74  * valuable and supported.
75  *
76  * @author Keith Donald
77  */

78 public abstract class AbstractFlowExecutionTests extends TestCase {
79
80     /**
81      * The factory that will create the flow execution to test.
82      */

83     private FlowExecutionFactory flowExecutionFactory;
84
85     /**
86      * The expression parser for parsing evaluatable model attribute
87      * expressions.
88      */

89     private ExpressionParser expressionParser = DefaultExpressionParserFactory.getExpressionParser();
90
91     /**
92      * The flow execution running the flow when the test is active (runtime
93      * object).
94      */

95     private FlowExecution flowExecution;
96
97     /**
98      * Set the expression parser responsible for parsing expression strings into
99      * evaluatable expression objects.
100      */

101     public void setExpressionParser(ExpressionParser expressionParser) {
102         Assert.notNull(expressionParser, "The expression parser is required");
103         this.expressionParser = expressionParser;
104     }
105
106     /**
107      * Gets the factory that will create the flow execution to test. This method
108      * will create the factory if it is not already set.
109      * @return the flow execution factory
110      * @see #createFlowExecutionFactory()
111      */

112     protected FlowExecutionFactory getFlowExecutionFactory() {
113         if (flowExecutionFactory == null) {
114             flowExecutionFactory = createFlowExecutionFactory();
115         }
116         return flowExecutionFactory;
117     }
118     
119     /**
120      * Creates an ExternalContext instance. Defaults to using {@link MockExternalContext}.
121      * Subclasses can override if they which to use another external context
122      * implementation.
123      * @param requestParameters request parameters to put into the
124      * external context (optional)
125      * @return a new ExternalContext instance
126      */

127     protected ExternalContext createExternalContext(ParameterMap requestParameters) {
128         return new MockExternalContext(requestParameters);
129     }
130
131     /**
132      * Start the flow execution to be tested.
133      * <p>
134      * Convenience operation that starts the execution with:
135      * <ul>
136      * <li>no input attributes
137      * <li>an empty {@link ExternalContext} with no environmental request
138      * parameters set
139      * </ul>
140      * @return the view selection made as a result of starting the flow
141      * (returned when the first interactive state (a view state or end state) is
142      * entered)
143      * @throws FlowExecutionException if an exception was thrown while starting
144      * the flow execution
145      */

146     protected ViewSelection startFlow() throws FlowExecutionException {
147         return startFlow(null, createExternalContext(null));
148     }
149
150     /**
151      * Start the flow execution to be tested.
152      * <p>
153      * Convenience operation that starts the execution with:
154      * <ul>
155      * <li>the specified input attributes, eligible for mapping by the root
156      * flow
157      * <li>an empty {@link ExternalContext} with no environmental request
158      * parameters set
159      * </ul>
160      * @param input the flow execution input attributes eligible for mapping by
161      * the root flow
162      * @return the view selection made as a result of starting the flow
163      * (returned when the first interactive state (a view state or end state) is
164      * entered)
165      * @throws FlowExecutionException if an exception was thrown while starting
166      * the flow execution
167      */

168     protected ViewSelection startFlow(MutableAttributeMap input) throws FlowExecutionException {
169         return startFlow(input, createExternalContext(null));
170     }
171
172     /**
173      * Start the flow execution to be tested.
174      * <p>
175      * This is the most flexible of the start methods. It allows you to specify:
176      * <ol>
177      * <li>a map of input attributes to pass to the flow execution, eligible
178      * for mapping by the root flow definition
179      * <li>an external context that provides the flow execution being tested
180      * access to the calling environment for this request
181      * </ol>
182      * @param input the flow execution input attributes eligible for mapping by
183      * the root flow
184      * @param context the external context providing information about the
185      * caller's environment, used by the flow execution during the start
186      * operation
187      * @return the view selection made as a result of starting the flow
188      * (returned when the first interactive state (a view state or end state) is
189      * entered)
190      * @throws FlowExecutionException if an exception was thrown while starting
191      * the flow execution
192      */

193     protected ViewSelection startFlow(MutableAttributeMap input, ExternalContext context) throws FlowExecutionException {
194         flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition());
195         return flowExecution.start(input, context);
196     }
197
198     /**
199      * Signal an occurence of an event in the current state of the flow
200      * execution being tested.
201      * @param eventId the event that occured
202      * @throws FlowExecutionException if an exception was thrown within a state
203      * of the resumed flow execution during event processing
204      */

205     protected ViewSelection signalEvent(String JavaDoc eventId) throws FlowExecutionException {
206         return signalEvent(eventId, createExternalContext(null));
207     }
208
209     /**
210      * Signal an occurence of an event in the current state of the flow
211      * execution being tested.
212      * @param eventId the event that occured
213      * @param requestParameters request parameters needed by the flow execution
214      * to complete event processing
215      * @throws FlowExecutionException if an exception was thrown within a state
216      * of the resumed flow execution during event processing
217      */

218     protected ViewSelection signalEvent(String JavaDoc eventId, ParameterMap requestParameters) throws FlowExecutionException {
219         return signalEvent(eventId, createExternalContext(requestParameters));
220     }
221
222     /**
223      * Signal an occurence of an event in the current state of the flow
224      * execution being tested.
225      * <p>
226      * Note: signaling an event will cause state transitions to occur in a chain
227      * until control is returned to the caller. Control is returned once an
228      * "interactive" state type is entered: either a view state when the flow is
229      * paused or an end state when the flow terminates. Action states are
230      * executed without returning control, as their result always triggers
231      * another state transition, executed internally. Action states can also be
232      * executed in a chain like fashion (e.g. action state 1 (result), action
233      * state 2 (result), action state 3 (result), view state <control returns so
234      * view can be rendered>).
235      * <p>
236      * If you wish to verify expected behavior on each state transition (and not
237      * just when the view state triggers return of control back to the client),
238      * you have a few options:
239      * <p>
240      * First, you may implement standalone unit tests for your
241      * {@link org.springframework.webflow.execution.Action} implementations.
242      * There you can verify that an Action executes its logic properly in
243      * isolation. When you do this, you may mock or stub out services the Action
244      * implementation needs that are expensive to initialize. You can also
245      * verify there that the action puts everything in the flow or request scope
246      * it was expected to (to meet its contract with the view it is prepping for
247      * display, for example).
248      * <p>
249      * Second, you can attach one or more FlowExecutionListeners to the flow
250      * execution at start time within your test code, which will allow you to
251      * receive a callback on each state transition (among other points). It is
252      * recommended you extend
253      * {@link org.springframework.webflow.execution.FlowExecutionListenerAdapter}
254      * and only override the callback methods you are interested in.
255      * @param eventId the event that occured
256      * @param context the external context providing information about the
257      * caller's environment, used by the flow execution during the signal event
258      * operation
259      * @return the view selection that was made, returned once control is
260      * returned to the client (occurs when the flow enters a view state, or an
261      * end state)
262      * @throws FlowExecutionException if an exception was thrown within a state
263      * of the resumed flow execution during event processing
264      */

265     protected ViewSelection signalEvent(String JavaDoc eventId, ExternalContext context) throws FlowExecutionException {
266         Assert.state(flowExecution != null, "The flow execution to test is [null]; "
267                 + "you must start the flow execution before you can signal an event against it!");
268         return flowExecution.signalEvent(eventId, context);
269     }
270
271     /**
272      * Refresh the flow execution being tested, asking the current view state to
273      * make a "refresh" view selection. This is idempotent operation that may be
274      * safely called on an active but currently paused execution. Used to
275      * simulate a browser flow execution redirect.
276      * @return the current view selection for this flow execution
277      * @throws FlowExecutionException if an exception was thrown during refresh
278      */

279     protected ViewSelection refresh() throws FlowExecutionException {
280         return refresh(createExternalContext(null));
281     }
282
283     /**
284      * Refresh the flow execution being tested, asking the current view state
285      * state to make a "refresh" view selection. This is idempotent operation
286      * that may be safely called on an active but currently paused execution.
287      * Used to simulate a browser flow execution redirect.
288      * @param context the external context providing information about the
289      * caller's environment, used by the flow execution during the refresh
290      * operation
291      * @return the current view selection for this flow execution
292      * @throws FlowExecutionException if an exception was thrown during refresh
293      */

294     protected ViewSelection refresh(ExternalContext context) throws FlowExecutionException {
295         Assert.state(flowExecution != null,
296                 "The flow execution to test is [null]; you must start the flow execution before you can refresh it!");
297         return flowExecution.refresh(context);
298     }
299
300     // convenience accessors
301

302     /**
303      * Returns the flow execution being tested.
304      * @return the flow execution
305      * @throws IllegalStateException the execution has not been started
306      */

307     protected FlowExecution getFlowExecution() throws IllegalStateException JavaDoc {
308         Assert.state(flowExecution != null,
309                 "The flow execution to test is [null]; you must start the flow execution before you can query it!");
310         return flowExecution;
311     }
312
313     /**
314      * Returns the attribute in conversation scope. Conversation-scoped
315      * attributes are shared by all flow sessions.
316      * @param attributeName the name of the attribute
317      * @return the attribute value
318      */

319     protected Object JavaDoc getConversationAttribute(String JavaDoc attributeName) {
320         return getFlowExecution().getConversationScope().get(attributeName);
321     }
322
323     /**
324      * Returns the required attribute in conversation scope; asserts the
325      * attribute is present. Conversation-scoped attributes are shared by all
326      * flow sessions.
327      * @param attributeName the name of the attribute
328      * @return the attribute value
329      * @throws IllegalStateException if the attribute was not present
330      */

331     protected Object JavaDoc getRequiredConversationAttribute(String JavaDoc attributeName) throws IllegalStateException JavaDoc {
332         return getFlowExecution().getConversationScope().getRequired(attributeName);
333     }
334
335     /**
336      * Returns the required attribute in conversation scope; asserts the
337      * attribute is present and of the required type. Conversation-scoped
338      * attributes are shared by all flow sessions.
339      * @param attributeName the name of the attribute
340      * @return the attribute value
341      * @throws IllegalStateException if the attribute was not present or not of
342      * the required type
343      */

344     protected Object JavaDoc getRequiredConversationAttribute(String JavaDoc attributeName, Class JavaDoc requiredType)
345             throws IllegalStateException JavaDoc {
346         return getFlowExecution().getConversationScope().getRequired(attributeName, requiredType);
347     }
348
349     /**
350      * Returns the attribute in flow scope. Flow-scoped attributes are local to
351      * the active flow session.
352      * @param attributeName the name of the attribute
353      * @return the attribute value
354      */

355     protected Object JavaDoc getFlowAttribute(String JavaDoc attributeName) {
356         return getFlowExecution().getActiveSession().getScope().get(attributeName);
357     }
358
359     /**
360      * Returns the required attribute in flow scope; asserts the attribute is
361      * present. Flow-scoped attributes are local to the active flow session.
362      * @param attributeName the name of the attribute
363      * @return the attribute value
364      * @throws IllegalStateException if the attribute was not present
365      */

366     protected Object JavaDoc getRequiredFlowAttribute(String JavaDoc attributeName) throws IllegalStateException JavaDoc {
367         return getFlowExecution().getActiveSession().getScope().getRequired(attributeName);
368     }
369
370     /**
371      * Returns the required attribute in flow scope; asserts the attribute is
372      * present and of the correct type. Flow-scoped attributes are local to the
373      * active flow session.
374      * @param attributeName the name of the attribute
375      * @return the attribute value
376      * @throws IllegalStateException if the attribute was not present or was of
377      * the wrong type
378      */

379     protected Object JavaDoc getRequiredFlowAttribute(String JavaDoc attributeName, Class JavaDoc requiredType) throws IllegalStateException JavaDoc {
380         return getFlowExecution().getActiveSession().getScope().getRequired(attributeName, requiredType);
381     }
382
383     // assert helpers
384

385     /**
386      * Assert that the active flow session is for the flow with the provided id.
387      * @param expectedActiveFlowId the flow id that should have a session active
388      * in the tested flow execution
389      */

390     protected void assertActiveFlowEquals(String JavaDoc expectedActiveFlowId) {
391         assertEquals("The active flow id '" + getFlowExecution().getActiveSession().getDefinition().getId()
392                 + "' does not equal the expected active flow id '" + expectedActiveFlowId + "'", expectedActiveFlowId,
393                 getFlowExecution().getActiveSession().getDefinition().getId());
394     }
395
396     /**
397      * Assert that the entire flow execution is active; that is, it has not
398      * ended and has been started.
399      */

400     protected void assertFlowExecutionActive() {
401         assertTrue("The flow execution is not active but it should be", getFlowExecution().isActive());
402     }
403
404     /**
405      * Assert that the entire flow execution has ended; that is, it is no longer
406      * active.
407      */

408     protected void assertFlowExecutionEnded() {
409         assertTrue("The flow execution is still active but it should have ended", !getFlowExecution().isActive());
410     }
411
412     /**
413      * Assert that the current state of the flow execution equals the provided
414      * state id.
415      * @param expectedCurrentStateId the expected current state
416      */

417     protected void assertCurrentStateEquals(String JavaDoc expectedCurrentStateId) {
418         assertEquals("The current state '" + getFlowExecution().getActiveSession().getState().getId()
419                 + "' does not equal the expected state '" + expectedCurrentStateId + "'", expectedCurrentStateId,
420                 getFlowExecution().getActiveSession().getState().getId());
421     }
422
423     /**
424      * Assert that the view name equals the provided value.
425      * @param expectedViewName the expected name
426      * @param viewSelection the selected view
427      */

428     protected void assertViewNameEquals(String JavaDoc expectedViewName, ApplicationView viewSelection) {
429         assertEquals("The view name is wrong:", expectedViewName, viewSelection.getViewName());
430     }
431
432     /**
433      * Assert that the selected view contains the specified model attribute with
434      * the provided expected value.
435      * @param expectedValue the expected value
436      * @param attributeName the attribute name (can be an expression)
437      * @param viewSelection the selected view with a model attribute map to
438      * assert against
439      */

440     protected void assertModelAttributeEquals(Object JavaDoc expectedValue, String JavaDoc attributeName, ApplicationView viewSelection) {
441         assertEquals("The model attribute '" + attributeName + "' value is wrong:", expectedValue,
442                 evaluateModelAttributeExpression(attributeName, viewSelection.getModel()));
443     }
444
445     /**
446      * Assert that the selected view contains the specified collection model
447      * attribute with the provided expected size.
448      * @param expectedSize the expected size
449      * @param attributeName the collection attribute name (can be an expression
450      * @param viewSelection the selected view with a model attribute map to
451      * assert against
452      */

453     protected void assertModelAttributeCollectionSize(int expectedSize, String JavaDoc attributeName,
454             ApplicationView viewSelection) {
455         assertModelAttributeNotNull(attributeName, viewSelection);
456         Collection JavaDoc c = (Collection JavaDoc)evaluateModelAttributeExpression(attributeName, viewSelection.getModel());
457         assertEquals("The model attribute '" + attributeName + "' collection size is wrong:", expectedSize, c.size());
458     }
459
460     /**
461      * Assert that the selected view contains the specified model attribute.
462      * @param attributeName the attribute name (can be an expression)
463      * @param viewSelection the selected view with a model attribute map to
464      * assert against
465      */

466     protected void assertModelAttributeNotNull(String JavaDoc attributeName, ApplicationView viewSelection) {
467         assertNotNull("The model attribute '" + attributeName + "' is null but should not be; model contents are "
468                 + StylerUtils.style(viewSelection.getModel()), evaluateModelAttributeExpression(attributeName,
469                 viewSelection.getModel()));
470     }
471
472     /**
473      * Assert that the selected view does not contain the specified model
474      * attribute.
475      * @param attributeName the attribute name (can be an expression)
476      * @param viewSelection the selected view with a model attribute map to
477      * assert against
478      */

479     protected void assertModelAttributeNull(String JavaDoc attributeName, ApplicationView viewSelection) {
480         assertNull("The model attribute '" + attributeName + "' is not null but should be; model contents are "
481                 + StylerUtils.style(viewSelection.getModel()), evaluateModelAttributeExpression(attributeName,
482                 viewSelection.getModel()));
483     }
484
485     // other helpers
486

487     /**
488      * Assert that the returned view selection is an instance of
489      * {@link ApplicationView}.
490      * @param viewSelection the view selection
491      */

492     protected ApplicationView applicationView(ViewSelection viewSelection) {
493         Assert.isInstanceOf(ApplicationView.class, viewSelection, "Unexpected class of view selection: ");
494         return (ApplicationView)viewSelection;
495     }
496
497     /**
498      * Assert that the returned view selection is an instance of
499      * {@link FlowExecutionRedirect}.
500      * @param viewSelection the view selection
501      */

502     protected FlowExecutionRedirect flowExecutionRedirect(ViewSelection viewSelection) {
503         Assert.isInstanceOf(FlowExecutionRedirect.class, viewSelection, "Unexpected class of view selection: ");
504         return (FlowExecutionRedirect)viewSelection;
505     }
506
507     /**
508      * Assert that the returned view selection is an instance of
509      * {@link FlowDefinitionRedirect}.
510      * @param viewSelection the view selection
511      */

512     protected FlowDefinitionRedirect flowDefinitionRedirect(ViewSelection viewSelection) {
513         Assert.isInstanceOf(FlowDefinitionRedirect.class, viewSelection, "Unexpected class of view selection: ");
514         return (FlowDefinitionRedirect)viewSelection;
515     }
516
517     /**
518      * Assert that the returned view selection is an instance of
519      * {@link ExternalRedirect}.
520      * @param viewSelection the view selection
521      */

522     protected ExternalRedirect externalRedirect(ViewSelection viewSelection) {
523         Assert.isInstanceOf(ExternalRedirect.class, viewSelection, "Unexpected class of view selection: ");
524         return (ExternalRedirect)viewSelection;
525     }
526
527     /**
528      * Assert that the returned view selection is the
529      * {@link ViewSelection#NULL_VIEW}.
530      * @param viewSelection the view selection
531      */

532     protected void nullView(ViewSelection viewSelection) {
533         assertEquals("Not the null view selection:", viewSelection, ViewSelection.NULL_VIEW);
534     }
535
536     /**
537      * Evaluates a model attribute expression.
538      * @param attributeName the attribute expression
539      * @param model the model map
540      * @return the attribute expression value
541      */

542     protected Object JavaDoc evaluateModelAttributeExpression(String JavaDoc attributeName, Map JavaDoc model) {
543         return expressionParser.parseExpression(attributeName).evaluate(model, null);
544     }
545
546     /**
547      * Factory method to create the flow execution factory. Subclasses
548      * could override this if they want to use a custom flow execution factory.
549      * The default implementation just returns a {@link FlowExecutionImplFactory}
550      * instance.
551      * @return the flow execution factory
552      */

553     protected FlowExecutionFactory createFlowExecutionFactory() {
554         return new FlowExecutionImplFactory();
555     }
556     
557     /**
558      * Directly update the flow execution used by the test by setting
559      * it to given flow execution. Use this if you have somehow manipulated
560      * the flow execution being tested and want to continue the test
561      * with another flow execution.
562      * @param flowExecution the flow execution to use
563      */

564     protected void updateFlowExecution(FlowExecution flowExecution) {
565         this.flowExecution = flowExecution;
566     }
567
568     /**
569      * Returns the flow definition to be tested. Subclasses must implement.
570      * @return the flow definition
571      */

572     protected abstract FlowDefinition getFlowDefinition();
573 }
Popular Tags