Chapter 9. Wizards and workflow with webflows


In this chapter

  • What webflows are, and what they’re useful for
  • Implementing basic wizard-style flows
  • Making use of branching for complex conditions
  • Simplifying logic with subflows
  • Leveraging flow-scoped stateful services
  • Testing webflows

We’ve covered a lot of territory so far, and learned a lot about the basics of controllers, views, and models—how they work together and how to test them. We’ve given our application a great UI makeover, and learned plenty about core Grails application development in the process. But we haven’t explored basic workflow yet.

What if you have a signup process that spans several pages? Or what if your shopping cart checkout needs to optionally give the user shipping preferences based on their location? You can solve these issues with your current knowledge of controllers and a bunch of redirects with hidden form variables, but you’d be writing your own state engine, and depending on the complexity of the flow, things can get complex quickly.

Grails webflows are designed for exactly this kind of scenario, and they make implementing multipage wizards with optional steps clean and maintainable. Webflows offer an easy-to-use and highly-configurable state engine—perfect for your next web-based workflow, and without all the overhead of a heavyweight rules engine.

Let’s learn how webflows can make implementing a Hubbub store an afternoon’s work.

9.1. What is a webflow?

A Grails webflow is a series of logical steps displaying various screens through which the user progresses to arrive at a final destination. Users might skip steps depending on the data they’ve entered (for example, if they do or don’t want custom shipping), and they might end up at entirely different end pages as a result of their selections (such as if their order is complete or their items are out of stock).

Let’s look at an ideal multistep application flow in flowchart form. We’ll use Hubbub for our example and have the user select some items from the store and enter their shipping details. We’ll then validate their credit card and give them a confirmation page. Figure 9.1 shows our first draft of an order flow for the Hubbub shop. We’ll implement the flow basics in a webflow and then add some more complex steps and subflows as the chapter progresses.

Figure 9.1. Our basic workflow as a flowchart

This first flow is pretty simple, but even at this stage it illustrates a few key advantages of having a webflow engine. What will a webflow give us that a standard Grails controller solution wouldn’t? A webflow will do several things:

  • Track what stage of the workflow the user is at (so the Next and Previous buttons will be context sensitive)
  • Provide a simple domain-specific language (DSL) to express what happens at each stage of the process, so it’s easy to see which step happens where
  • Make it easy to insert new steps into the process without refactoring a lot of controller actions
  • Give us a lot of power for expressing decision states simply and elegantly (like our card-validation step) and for handling routing based on the outcome of the decision
  • Introduce two new scopes—flow and conversation—that allow us to store variables that can only be accessed within the flow, and which will be removed automatically at the conclusion of the flow

Webflows offer a lot of power for any part of an application that lends itself to a multistep interaction.

But one thing that you should know up front is that webflows aren’t suited for every application setting—they’re targeted to basic wizard-style multistep operations. They’re perfect for breaking a complex 100-field form into multiple sequential pages, or for tricky checkout features where you have to validate credit cards, collect address and shipping options, and then offer a confirmation page. But don’t try to force your new Flickr-clone site into a webflow style of interaction—you’ll spend more time fighting the framework than solving real problems.

9.1.1. Writing your first flow: a checkout wizard

Now that you have a basic understanding of the situations where webflows do and don’t make sense, it’s time to write our simple order flow and learn how all the pieces fit together.

You’ll be pleasantly surprised to find that all webflow definitions happen in the context of a standard controller. Let’s start by creating a Hubbub shop controller for purchasing Hubbub-approved swag:

grails create-controller com.grailsinaction.shop

With our shop controller in place, it’s time to start defining our first flow. As outlined in figure 9.1, we’ll use the Webflow DSL to define actions for each of the steps in the workflow. We’ll call our flow orderFlow—flow definitions, by convention, always start with an action ending in “Flow”. This tells Grails that it’s a Webflow DSL definition. Flows also get their own endpoint, so our orderFlow will have a URL of /hubbub /shop/order.

We can now define the flow. Listing 9.1 defines the parent-level flow element.

Listing 9.1. Our empty flow definition waiting for some workflow steps
package com.grailsinaction

class ShopController {

def index = {
redirect(action: "order")
}

def orderFlow = {
}
}

That’s quite bare bones. You’ll notice we’ve included an index action on our controller. As you’ll recall, if there’s only one action in a controller, it becomes the default action, but given the special nature of webflow URLs, that rule doesn’t apply. If you want a tidy URL (/shop redirecting to /shop/order), you have to take matters into your own hands.

Also notice that although the definition of the flow is orderFlow, we refer to the action without the “Flow” part from within controllers—so the redirect from index() is to "order", not "orderFlow".

Our flow is defined in listing 9.1, but it doesn’t do anything yet. An easy way of getting started with flows like this is to develop skeleton code for the entire flow first, and then fill in the logic later. Without further ado, let’s throw together a bare-bones definition of the flow, and then put some meat on each step. Listing 9.2 shows a skeleton flow definition ready for some flow-step logic.

Listing 9.2. A skeleton flow definition

Even though this is a bare-bones workflow, there’s still some basic wizard-like logic going on. You can see how each webflow state matches a step in our flowchart diagram (figure 9.1). After each on() step, the webflow transitions the flow to() a new state. We’ll get into the details shortly, but for now see if you can get the gist of the flow as a whole, and how it maps to our flowchart.

Now that you have a feel for how flows are implemented, it’s time to learn how flows transition from state to state. Let’s start our exploration with a few of those on().to() things.


The execution param: back-button and permalinking issues

As you start working with webflows, you’ll notice a constantly changing state-tracking parameter that the webflow appends to each URL in your flow (execution=e1s1, with the value changing on each state change).

Suppose a user bookmarks a flow in the middle of your page, or links to it from an external resource, or uses the browser’s back button after the flow has ended and tries to resubmit a form from the middle of the flow. In these cases, Grails intercepts a request to an expired or finished flow and redirects the user to the first state in the flow. If you look at the webflow logs (see the sidebar on “Debugging bizarre webflow errors”), you’ll see the framework catch them:

[6431198] servlet.FlowHandlerAdapter Restarting a new execution of
previously expired/ended flow 'order'

Normally, heading back to the start of the flow is the best way to handle this kind of error. If you need to perform custom expired-flow management, your best bet is to create a custom filter for the error.


9.1.2. Anatomy of a flow state

Each step in the flow is referred to as a state. The first flow state encountered in a webflow is known as the start state, which, in our case, is displayProducts.

When the user first hits /shop/order, Grails will invoke displayProducts to get things going. Let’s have a closer look at its definition:

displayProducts {
on("next") {
// capture products
}.to("enterAddress")
on("cancel").to("finish")
}

How does displayProducts fire those "next" and "cancel" rules? The answer lies in how view conventions work. In the case of displayProducts, Grails will first render a view page called /views/shop/order/displayProduct.gsp, and then wait for the user to submit the page. Based on the buttons clicked on that page, the flow can progress.

Listing 9.3 creates a basic displayProduct page to get us started. (Remember, this is in an “order” subdirectory of /views/shop, so we’re one level deeper than normal.)

Listing 9.3. Triggering flow steps from a view

This looks like a pretty standard Grails form GSP, with only a few items that relate to webflow. First, notice that the form submission target is "order", not orderFlow . Views in the flow always point to the same target action, and the webflow engine handles which state the user should be sent to based on their progress (via the execution parameter that the webflow adds to each URL, as discussed in the previous sidebar on the execution param).

The other thing to note is that the button names are now important . The name of the button will invoke the appropriate action in the backend, so be careful not to misspell them.

The final part of an action is the .to() clause, which tells the webflow which state to fire next. When the user clicks on a Cancel button, things start to fire. The form is submitted, the webflow follows the on() clause and ends up at the finish state, which sends the user back home. Listing 9.4 demonstrates the flow transition.

Listing 9.4. The on() clause maps the Cancel button name to the next logical action.

You’ll notice that the finish state has no .to() clauses. Any state without a .to() clause is referred to as an end state. These states are special because once an end state fires, all flow state data is discarded (such as which step the user is at in the flow, and anything in flow scope, which we’ll discuss shortly).


Breaking conventions: using custom view rendering

Webflow view names follow the standard convention (view filename matches the state name), but you’re free to break from that convention when you need to via a custom render() call. For example, our displayProducts state in listing 9.4 could be directed to a different view name as follows:

displayProducts {
render(view: "standardProducts")
on("next") {
// capture products
}.to("enterAddress")
on("cancel").to("finish")
}

9.2. Working with webflows

Now that you understand a little about how webflows handle states and routing, let’s apply that knowledge to implement our order flow. In this section, we’ll learn the basics of implementing a useful flow:

  • How to store objects in flow scope
  • How to perform data binding and error handling in a flow
  • How to implement decision states to route the user to different states programmatically

We’ll start with an exploration of flow scope—a new scope that Grails provides for storing objects that are accessed throughout the lifetime of a webflow.

9.2.1. Flow scope: better than Flash scope, cheaper than session scope

Grails webflows add a new scope to your repertoire: flow scope. Items put in flow scope live for the life of the flow (that is, until the user transitions to an end state, as in our previous finish example). Using flow scope is a effective way to share data across a flow, and it’s perfect for storing objects that are built up throughout the flow. In our Hubbub example, we could store a product selection in the first step of the flow, and use it in a later step to confirm the order and work out the bill.

You can reference flow scope explicitly, like the other scopes, from within any state in your flow. Here’s an example:

This is an explicit way of accessing flow scope, but there are even better options. When you return objects from flow states (via a map style commonly used in controllers), they implicitly end up in flow scope. You can do things like this:

This will automatically place orderDetails and orderStartDate in flow scope for you. But flow scope has its own gotchas, and they’re significant. The biggest one is that every object you place in flow scope must implement Serializable.


What’s the deal with Serializable?

The requirement that all flow-scoped objects implement Serializable means that basic types like Strings and Dates work fine, but domain object that are stored in flow scope must implement Serializable. Worse still, to satisfy the Serializable contract, any domain classes it holds a reference to need to be Serializable too. That can lead to a lot of Serializable changes!

If you haven’t worked with the Serializable interface in Java before, you might want to start with the Javadoc for java.io.Serializable. For most of your webflow uses, you’ll simply need to add implements Serializable to your domain class definition. But there are some complex and subtle use cases for Serializable that can trip up new Grails developers—particularly when Serializable objects are placed in collections.


A lot of the items you’ll store in flow scope are objects you’ve validated in previous steps, and you won’t want to go back and mark all domain objects Serializable just to store them in flow scope. Fortunately, using Serializable command objects offers us a neat solution to this problem.

Let’s continue our webflow exploration by digging a little deeper into the practical ways we can handle validation in a webflow setting.

9.2.2. Strategies for binding and validation

We’ve discussed some of the complexities of persisting domain objects in flow scope, but webflows give you some other options for handling and validating forms. One of the nicest is support for command objects in flows. We saw command objects in chapter 5, when we were looking at ways to handle form submissions—they should be the perfect solution for our current problem of avoiding the need to make domain classes Serializable.

Imagine that our displayProducts view gives us options for the numbers of shirts and hats to purchase. It might be as simple as this:


<g:form action="order">
Shirts: <g:textField name="numShirts" value="0"/>
Hats: <g:textField name="numHats" value="0"/>
<g:submitButton name="next" value="Next"/>
<g:submitButton name="cancel" value="Finished Shopping"/>
</g:form>

We’d like to ensure that when the user submits a value for the form, they don’t order more than 10 shirts or 10 hats. That sounds like a classic validation problem, right? The example Serializable object in listing 9.5 specifies a range of 0 to 10 for the order values, and we can add any other validation logic that’s relevant to the task at hand.

Listing 9.5. Command objects in flow steps must implement Serializable.
class OrderDetailsCommand implements Serializable {

int numShirts
int numHats

boolean isOrderBlank() {
numShirts == 0 && numHats == 0
}

static constraints = {
numShirts(range: 0..10)
numHats(range: 0..10)
}
}

Listing 9.6 shows how we can wire up our command object to our displayProducts state.

Listing 9.6. Mapping a command object to a flow step

Like standard (non-webflow) actions, webflow states that require a marshaled command object specify the object as the first closure argument . Grails marshals the request parameters into the command objects and sets the errors if there are validation errors .

One thing you haven’t seen before is the call to the error() method . Returning error() tells the webflow to return to the previously rendered view so the user can fix any failing validations, but the user will need access to the validation errors to fix the invalid fields. To support this, we put the failing command object back in flow scope so we can make use of its errors collection in the rendered view page.

This all makes more sense when you see it in action. Listing 9.7 shows an updated displayProducts view, this time with more-robust error handling.

Listing 9.7. An updated displayProducts view with error handling
<g:hasErrors bean="${orderDetails}">
<div class="errors">
<g:renderErrors bean="${orderDetails}"/>
</div>
</g:hasErrors>

<g:form action="order">
Shirts: <g:textField name="numShirts"
value="${orderDetails?.numShirts}"/>
Hats: <g:textField name="numHats" value="${orderDetails?.numHats}"/>
<g:submitButton name="next" value="Next"/>
<g:submitButton name="cancel" value="Finished Shopping"/>
</g:form>

Notice that we’re now rendering any error collection that’s present on our flowscoped orderDetails object. We also prepopulate the default field values with the flow-scoped object so we can repopulate the incorrect values for the users. Figure 9.2 shows the error messages in action.

Figure 9.2. Our form displays errors and repopulates failing fields.

This is what we wanted. You might have noticed that the error messages have been customized, too. The same i18n message bundle support we introduced in section 4.1.2 is available when using webflows. We added the following entries to /grails-app/i18n/messages.properties to generate the errors in figure 9.2:

OrderDetailsCommand.numShirts.range.toosmall= You may not order less than {3} shirts.
OrderDetailsCommand.numShirts.range.toobig= You may not order more than {4} shirts.
OrderDetailsCommand.numHats.range.toosmall= You may not order less than {3} hats.
OrderDetailsCommand.numHats.range.toobig= You may not order more than {4} hats.

Note

For a refresher on i18n message bundle support, look back to chapter 4.


With our flow now validating, and our flow-scope populated with command objects, it’s time to look at a flow state that doesn’t have any UI at all: the action state.

9.2.3. Making decisions programmatically with action states

So far, our flow-routing decisions have been UI-driven: the user clicks the Next or Previous button, or the form submission fails validation. But we haven’t yet looked at altering the flow programmatically. We might want to check stock levels, or validate a credit card, or offer special shipping options for people living in remote places.

This kind of decision step in a flow doesn’t have a UI, just the ability to change the flow based on some backend processing. In the context of webflows, these are called action states, to separate them from the more common transition states we’ve been dealing with so far.

Let’s use credit card handling as an example. After the user submits their credit card details, we want to verify them against a merchant service. If the card number and balance are OK, the flow can continue, but if the card fails validation, we want to return to the card-input form. We can implement this kind of decision flow with an action state.

Let’s implement a simple credit card validation step that fails whenever a user presents a card in the morning. We want to end up with a result like that in figure 9.3. A form like this will normally go through a standard view-state action to validate the basic field entries. It would look something like this:

enterPayment {
on("next") { PaymentCommand pc ->
flow.pc = pc
if (pc.hasErrors()) {
return error()
}
}.to("validateCard")
on("previous").to("enterAddress")
}
Figure 9.3. Flows support custom error messages created with message bundles.

We do the basic validation checks, and if things are fine, we transition to our new action state: validateCard.

Action states start with the action keyword, so that Grails knows there’s no view to render. From there, the body of the flow is like any other flow state. Action states typically define and return custom transition names to make the body of the flow more readable. In listing 9.8, we return invalid() or valid() depending on the processing of the credit card.

Listing 9.8. Implementing a credit card validation action step

In listing 9.8, when the validation fails, we add a custom error object to the Payment-Command (pc) in flow scope. This enables our views to use the existing Grails has-Errors tags to conditionally display the contents of our message.

That pretty much covers what action states do and how you use them. Action states are great for representing complex decision logic without cluttering up already overworked transition actions. They’re also great for branching to kick off subflows, which we’ll look at shortly.

You have now seen all the common webflow features that you’re likely to use in your next Grails application. In the next section, we’ll cover a number of advanced webflow concepts—you’ll use these less frequently, but they are convenient for specific webflow scenarios.

9.3. Advanced webflows

You should now have a solid understanding of all the basics of implementing application flows, but there are a few advanced webflow features that come in handy for particular flow scenarios. In this section, we’ll explore some of them, including subflows and conversations. We’ll start by looking at flow-scoped services.

9.3.1. Flow-scoped services

So far we’ve learned about storing Serializable objects in flow scope so they can be passed along to subsequent flow states. We haven’t yet introduced you to using flow-scoped services to give you the equivalent of lightweight stateful session beans.

Flow-scoped services are useful when you want a service object that you can use throughout a flow, but that will maintain data that’s relevant only to the current flow. Our credit-card validator is a good example. We use it early in the flow to validate the credit card, and then later in the flow we might access an error code or validation number stored statefully in the service.

Let’s clean up our earlier example and experiment with generating a flow-scoped credit-card validator. Flow-scoped services are like regular services and are created in the standard way:

grails create-service com.grailsinaction.CreditCard

This is the same as every other service you’ve created. But unlike standard services, you explicitly specify the scope of service via the scope attribute:

class CreditCardService implements Serializable {

static scope = "flow"
static transactional = false
}

Like all objects stored in flow scope, these services must implement Serializable. Once the service is defined in this manner, the standard service injection rules apply for controllers that use the service (review chapter 4 if you need a refresher on services).

It’s important to note that, although the service definition happens in the controller (and not in the flow DSL), each new flow will get a new instance of the injected service—the service is created per flow, not per controller.

Here’s our updated ShopController with the service injected:

class ShopController {

def creditCardService

// ... rest of controller actions
}

Once the service is defined in the controller, making use of the service within any of your flow states will ensure that you access an instance of the service that’s created uniquely for that flow. This means you can use the service in action steps. Listing 9.9 shows our validateCard action reworked to use our flow-scoped service.

Listing 9.9. Invoking a stateful service to validate the card

Notice that we reference our creditCardService, and Grails takes care of all the flowscoped state management behind the scenes. No magic flow.creditCardService is required.

The biggest advantage of flow-scoped services is that you can safely access the service in a stateful way in subsequent flow states. That freedom means we can implement our CreditCardService as a stateful bean, confident we’ll never have concurrency issues from webflow states that consume the service. Listing 9.10 shows a basic stateful implementation of the service.

Listing 9.10. Abstracting credit-card validation to a stateful service

As we explained in Chapter 4, you typically write stateless services because by default they’re shared singletons. But in listing 9.10, we store the card details statefully in the service. Later in the flow, we can safely reference that data or call other methods on the service that reference those internal attributes, confident that nothing will have been overwritten in the meantime.

Because flow-scoped services are serialized in the flow, they need to implement Serializable, like everything else that is stored in a flow. This is different from standard services, so when you see errors that say “object in flow state could not be serialized,” you know what to do.


What if I want a service that’s flow- and controller-scoped?

I know what you’re thinking—you want to use your service in flow scope, but also use some of its methods from a standard controller. Sorry, but that’s almost always a code smell.

The main reason for using flow-scoped services is to keep stateful data. If you want to mix and match stateful and stateless methods, you’re probably talking about methods that belong in two different service classes.

One solution is to move your flow-specific stateful code into its own service, and then inject your stateless service into that. This means you can reuse your stateless business logic, use a stateful bean, and keeps things DRY at the same time.


9.3.2. Subflows and conversations

So far we’ve looked at branching and stepping through a single flow. But when your flow grows, things can get complex. You’ll be branching in and out of various parts of your flow, your workflows will become confusing, and your flow state can start to get cluttered. For these scenarios, Grails provides the option of subflows, which let you group some related flow functionality in a separate standalone flow.

Subflows offer several advantages:

  • You can reuse a subflow in several different parent flows.
  • Your flow state is cleaner because it’s scoped only to your subflow, and it’s cleaned up on subflow termination
  • Grouping logic into subflows makes your flows shorter and more self-contained, and your logic is easier to follow
  • You can share data between parent flows and subflows, so you don’t sacrifice or duplicate flow data

Our postage and shipping calculations are a logical place to use subflows. We’ll give the user the option of choosing custom shipping, and if they do, we’ll send them off to a subflow that handles shipping options. Figure 9.4 shows an updated section of our flowchart (from figure 9.1), with our new subflow appearing after the address step.

Figure 9.4. Adding a new custom shipping subflow after the address step

On the address page, we’ll give the user a Custom Shipping check box, and we’ll use that flag to trigger our subflow. We’ll need a command object to hold the shipping details: listing 9.11 shows this ShippingCommand class. We’ll add constraints later, but for now we’ll use it as a simple DTO.

Listing 9.11. A ShippingCommand object holds the user’s shipping preferences
class ShippingCommand implements Serializable {

String address
String state
String postcode
String country
boolean customShipping
String shippingType
String shippingOptions

}

Let’s get set up to trigger the subflow from our main orderFlow. To do that, we need to share our shipping details from the parent flow to the subflow. At the moment, we store our ShippingCommand data in flow scope, but flow scope doesn’t get passed to subflows. For that, we need conversation scope, which covers data shared between the current flow and all subflows. Let’s make the modifications to put our shipping details in the conversation, as shown in listing 9.12.

Listing 9.12. Storing details in conversation scope means the data can pass to subflows.

With our shipping details in conversation scope, we’re ready to do some branching based on whether the user has any custom shipping requirements. This is a great chance to put our knowledge of action flows to work. Listing 9.13 introduces a check-Shipping action to localize our shipping logic.

Listing 9.13. Using an action step to determine whether we need custom shipping

In listing 9.13, we first check our shipping command object to see if the user has requested custom shipping . If they have, we invoke the custom() transition which will transition them to the customShipping state . If the user wants standard shipping, we invoke the standard() transition , and the flow continues to enter-Payment as usual .

It’s now time to introduce our customShipping flow state. This new flow state handles branching to a new subflow, and there are a few things you need to know about the process. First, we’ll look at the definition in listing 9.14 to give you a taste of the new construct: subflow().

Listing 9.14. Triggering a subflow from the parent flow

The first thing you need to know about subflows is that naming is important. The subflow name must match the state name without the “Flow” suffix—the subflow branch to customShippingFlow must happen in a state called customShipping. This is a bit of a tedious convention that will probably go away in some future Grails version, but for now that’s the deal.

But where do all those state results come from (goBack, standardShipping, customShipping)? The short answer is that they’re the values of the end state returned from the subflow. That will make more sense when you’ve seen the subflow definition, so let’s do that next.

In listing 9.15 we add a new definition to our OrderController for the new customShipping subflow.

Listing 9.15. Implementing a custom shipping subflow

As you saw in our flowchart (figure 9.4), custom shipping is straightforward. It’s a two-step process with the user first choosing a shipping type and then continuing on to select shipping options . Then they’re done.

A subflow looks exactly like any other flow. There are only two differences in a subflow:

  • The use of conversation-scoped variables (rather than flow-scoped variables) to share and update data from the parent flow
  • The use of end states to trigger state change in the parent flow

In listing 9.15, when the user clicks the Next button in the selectShippingOptions flow , the customShipping() end state is triggered. This ends the flow because there are no more .on() triggers to that state, so the value is returned to the custom-Shipping action in the parent OrderFlow, which looked like this:

customShipping {
subflow(customShippingFlow)
on("goBack").to("enterAddress")
on("standardShipping") {
conversation.sc.customShipping = false
}.to("enterPayment")
on("customShipping").to("enterPayment")
}

customShipping fires the transition to the next state, enterPayment, and the flow continues as normal. By moving all of that shipping logic into its own flow, we’ve managed to move a lot of logic out of orderFlow, and we’ve made a reusable shippingFlow in the process.

But there’s still a piece of the puzzle missing. We’ve covered the logic of subflows, but you might be wondering where the GSP files go for this new subflow. They follow the same convention as for parent flows. If customShippingFlow is defined in Order-Controller (as was the case here), the selectShippingType.gsp file goes in /views/order/customShipping/.

We’ve covered a lot of advanced information about webflows and subflows, but there’s one area we haven’t explored: testing. How are we going to test all those flows and subflows to ensure that everything works as we expect? It’s time to explore webflow testing support.


Debugging bizarre webflow errors

When you have a complex graph of flows and subflows, you will inevitably end up with some kind of bizarre serialization error, 404 error, or other problem. In these situations, there are two important options to work through.

First, you can run grails clean. You might just have a compilation issue, so clearing out classes and recompiling will often resolve your issue.

If that doesn’t work, you can try the second option and configure logging levels in /grails-app/conf/Config.groovy so you can see all of the webflow’s internal debug logs. Add a line like this:

log4j = {
debug 'org.springframework.webflow'
}

Then restart your server. You’ll get verbose output, but it will give you clues as to what states are triggering and what is being serialized in the process. It will take some time to work through it all, but at least you’ll have a better idea of what the problem is.

If your problem relates to view resolution (for example, you’re getting 404 errors), try configuring log levels for org.springframework to debug. Better still, install the runtime-logging plugin and change your log levels on the fly via /hubbub/runtimeLogging. It gets noisy, but you will get to the bottom of the issue (or possibly raise a JIRA in the process).


9.4. Testing webflows

As with all your other Grails artifacts, you should be giving some thought to how you can test your webflows. The state-tracking side of a webflow makes testing trickier than for your standard controller, but Grails offers some built-in support for mocking the required pieces of the puzzle. Let’s take a look at a basic test case to give you an idea of how to get started.

Our work so far has been in the order controller, so we’ll start our testing there. All webflow testing must be done in integration tests, so let’s create a skeleton integration test case:

grails create-integration-test com.grailsinaction.OrderFlowIntegration

With our integration test in place, we’ll update it to extend WebFlowTestCase—the base class for all webflow-related tests. Listing 9.16 shows a simple test case.

Listing 9.16. A simple integration test for our flow

As we said, all webflow test cases are integration tests that extend WebFlowTestCase . Extending WebFlowTestCase requires that you create a getFlow() method to return the closure action that represents your webflow. In our case, we’re working with the orderFlow. We then create an instance of ShopController that we can keep around for later , because we’ll need to simulate supplying params to our actions. Finally, we start our testBasicOrder() implementation by triggering the start-Flow() method .

As the name suggests, startFlow() is the first action on your flow, and it’s one of several methods added by WebFlowTestCase that you use to trigger actions on your flows. The most important of these methods are listed in table 9.1.

Table 9.1. Flow-related methods available in WebFlowTestCase

Method

Description

startFlow()

Triggers the first state in the current flow

signalEvent(String)

Simulates a user triggering a flow event

setCurrentState(String)

Starts a new flow, moving immediately to the specified state

getFlowScope()

Obtains the current flow scope

getConversationScope()

Obtains the current conversation scope

After starting the flow, we can assert that the current state has been moved to displayProducts() using assertCurrentStateEquals() . This assertion and others are also added by WebFlowTestCase to ensure that your flow is tracking to the correct state. Table 9.2 lists some of the most important assertions available in webflow tests.

Table 9.2. Flow-related assertions available in WebFlowTestCase

Assertion

Description

assertCurrentStateEquals(String)

Checks that the current state is active, and that the state name matches the specified string

assertFlowExecutionActive()

Asserts that the flow hasn’t reached an end state

assertFlowExecutionEnded()

Asserts that the flow has reached an end state and terminated

assertFlowExecutionOutcomeEquals(String)

When a flow has ended, ensures the outcome name matches the supplied string

Now that you’ve seen the basics of webflow tests, let’s create a more complete test case for our order flow.

9.4.1. Handling input parameters

You learned a lot about testing controllers in chapter 7, so passing parameters in to controllers should be nothing new. But testing in a webflow setting gives you a few more corner cases to deal with.

We’ll submit some invalid parameters to our displayProducts state, and see how we can enhance our test case to give the flow a lot more test coverage. Listing 9.17 shows our test case with code to submit product order numbers and handle error conditions when the order numbers exceed the maximum allowed.

Listing 9.17. Submitting values and handling error conditions

After starting our flow, we start the test by simulating a user submitting an order form with an invalid quantity of hats . The constraint on the command object is that order quantities for items must be between 0 and 10.

Signaling the next event submits our parameters to the displayProduction action (shown earlier in listing 9.6), resulting in a validation error, and redirecting back to the displayProducts state. To refresh your memory, these are the actions we’re testing:

Notice that when validation fails, we place the command object back in flow scope so we can preserve the previous form values when we rerender the order form. Because the failed order ends up in flow scope, it means that our test case can assert that the resulting orderDetails object appears in flow scope with the relevant errors attribute, and the user has been routed back to the displayProducts step .

With our error handling tested, we correct the invalid hat quantity and retrigger the submit . This time, all validations should pass, and we should end up at the enterAddress action .

That’s quite a comprehensive test, and it will no doubt give you lots of ideas about how to write your own. We’ve toured signaling events, learned how to handle parameters, and even dealt with errors. But we haven’t explored subflows and flow termination yet. Let’s look at those now.

9.4.2. Testing subflow transitions

You’ll recall that we moved the custom-shipping logic into a subflow in section 9.3.2. We’ll now write a test for that subflow. But we should first test that when the Custom Shipping check box is checked, the user is sent to the subflow, and when it isn’t checked, they go straight through to enterPayment.

Listing 9.18 shows the updated test code for skipping the custom shipping, then backing up and selecting it, which should leave you in the subflow.

Listing 9.18. Tripping a subflow

We start our test by jumping directly to the enterAddress state . We then simulate checking the Custom Shipping check box and clicking the Next button . Without custom shipping, the user should end up at the enterPayment state, so we explicitly check that the subflow has been skipped .

We then simulate clicking the Previous button and check that this returns us to the enterAddress state . This time, though, we simulate checking the customShipping check box, which starts the subflow and leaves us on the first step of the subflow—the selectShippingType page .

We could go on submitting params to the subflow to exercise the entire order process, but we’ll leave that as an exercise for you.

With our subflow integration now comprehensively tested, it’s time to write a test case to fully test the subflow itself. This will give us a chance to explore flow-termination testing.

9.4.3. Testing flow termination

The last thing we need to test is that flows terminate correctly. Our custom shipping flow has interesting termination characteristics, so it’s a good candidate for experimentation. Listing 9.19 reproduces the flow in its entirety.

Listing 9.19. Our custom shipping flow terminates in lots of interesting ways.
def customShippingFlow = {

selectShippingType {
on("next") {
conversation.sc.shippingType = params.shippingType
}.to("selectShippingOptions")
on("standardShipping").to("standardShipping")
on("previous").to("goBack")
}

selectShippingOptions {
on("previous").to("selectShippingType")
on("next") {
conversation.sc.shippingOptions = params.shippingOptions
}.to("customShipping")
}

customShipping()
standardShipping()
goBack()
}

First, we need to create an integration test:

grails create-integration-test com.grailsinaction.CustomShippingFlowIntegration

With our shell test case in place, it’s time to add some testing logic to exercise our new subflow. Listing 9.20 demonstrates the logic to simulate the user submitting a shipping type and shipping details and ending up at the customShipping endpoint.

Listing 9.20. A test case for custom flow termination

Our test case starts the subflow, and then submits shipping type and custom shipping options. Once those flow events have fired, we can assert that the flow has completed and that the final state of the flow is customShipping . The assertFlowExecution-Ended() call is redundant, because the final assertFlowExecutionOutcomeEquals() assertion ensures that the flow is in an end state, but we include it here for completeness.

That concludes our tour of webflow testing techniques. It’s time to review what we’ve learned.

9.5. Summary and best practices

This chapter has taken you on a comprehensive tour of Grails webflow—a highly configurable state-engine for handling Grails UI workflows. You’ve learned the basics of how states and transitions work, and you’ve implemented plenty of features that are common to wizard-style checkouts.

We’ve introduced you to the unique webflow states, flow and conversation, and used them to store validated command objects that are passed through the flow. We’ve also explored some of the advanced webflow features, such as subflows and session-scoped services.

We concluded the chapter with a thorough introduction to webflow testing, including all the basics of trigger flows, testing error conditions, and examining flow-termination states.

It’s been a comprehensive tour, and we’ve seen plenty of webflow best practices along the way:

  • Apply webflows selectively. Know what kinds of problems webflows are useful for, and only use them where they make sense. The most productive webflow features are specific state-engine style interactions, so don’t over-apply them.
  • Tame complex forms. Use webflows for complex form handling—splitting your form over multiple pages and skipping steps that aren’t applicable for every user.
  • Favor flow scope. Favor flow scope over session scope. Webflows will clean up the storage for you and give you more efficient use of server memory.
  • Understand execution params. Learn the impact of the execution param and how it influences Back buttons and permalinks. Design your flows to minimize that impact.
  • Use command objects. Make use of command objects to handle form validation for each state of your flow. Use validators on command objects to make them self-describing, and add custom routines to clarify business intent (like isOrderBlank()).
  • Simplify with action states. Simplify your decision states by using action states. Make good use of transition names to make the logic clearer.
  • Utilize flow-scoped services. Use flow-scoped services for stateful beans in your flows. The concurrency model is simple, and the services can be cleaned up on flow termination.
  • Write tests. As with all of your Grails artifacts, write test cases to ensure that your flow works the way you think it does. Writing tests is much quicker in the long run than continuous human-driven UI testing in the browser, and it will help regression testing when new Grails versions come out.

By completing the Hubbub online store, we’ve given you a thorough introduction to Grails Webflows and added a host of new features to Hubbub. In the next chapter, we’ll turn our attention to applying security constraints to Hubbub—ensuring users can only access the features in Hubbub they’re permitted to.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset