blog

Let's talk about testing

Complex Workflows with Cucumber

This entry is part 6 of 10 in the series Test Automation

Our favourite test automation software in the world remains cucumber, but like all software, it has its limitations. In particular, it is at first glance pretty difficult to express workflows in it that contain branches. In this post, we explain how that can be achieved.

The Problem

Imagine your software workflow is such that depending on some criteria, your tests should follow a different path through the software. The following flow chart is supposed to provide a very much simplified example of this:

Test workflow chart

This simple workflow takes into consideration where a client connects from, and offers a different path through the software towards reaching the workflow goal depending on that information. In this case, the different path is represented by an additional payment option.

In cucumber, each path needs to be a separate test scenario — branching off onto a different path is simply not something cucumber supports.

First Scenarios

Let’s ignore the special case for UK clients for the moment and look at how we might phrase the other scenario in gherkin:

Feature: Checkout
  Scenario Outline: International checkout
    Given I am on the checkout page
    When I enter my email address
    And I enter my password
    And I select <payment method>
    And I provide <payment details>
    Then I expect the payment details to be accepted
    And I expect to see an order confirmation page
 
    Examples:
      | payment method | payment details |
      | credit card    | my CC number    |
      | paypal         | my paypal email |

The example is particularly powerful if phrased as a scenario outline in that it already gives you a hint for how simple branches might be captured in cucumber/gherkin: use scenario outlines and example tables.

The example is also rather artificial in nature, because a credit card checkout process typically requires you to provide not just the credit card number, but also the owner’s name, and a verification code. In some cases, it might also include an additional verification step after that. Clearly the example given here does not capture all your needs.

For the sake of the argument, though, assume that it does.

Second Scenario

For the second scenario, we can re-use a bunch of the first. For starters, the beginning lines will be identical:

    Given I am on the checkout page
    When I enter my email address
    And I enter my password

But also, the end should be entirely the same:

    Then I expect the payment details to be accepted
    And I expect to see an order confirmation page

Our problem with branches can be viewed in a different light, then: what is it that is re-usable about all the scenarios and what is it that is different?

Finding a Solution

If viewed in this light, our problem isn’t so much about dealing with branches, it’s about how to best re-use existing steps. The first thing to do is to adopt the best practice of push how down, which refers to having less detailed steps, and leave the detail up to the step definitions.

With that in mind, our starting block can be rewritten like this:

    Given I provide my credentials on the checkout page

And the second part might become:

    Then I expect the checkout process to complete

The good news in all this is that you don’t have to write more complex step definitions. Instead, you can re-use existing steps:

Given /^I provide my credentials on the checkout page$/
  step "I am on the checkout page"
  step "I enter my email address"
  step "I enter my password"
end
 
# Or perhaps more intuitively:
 
Given /^I provide my credentials on the checkout page$/
  steps %Q{
    Given I am on the checkout page
    When I enter my email address
    And I enter my password
  }
end

Unfortunately, error reporting from this kind of aggregate step isn’t as good as from a more complex step. The recommendation is to instead of using and calling steps, you should just use and call Ruby methods:

# features/support/env.rb
def go_to_checkout_page
end
 
def enter_email
end
 
def enter_password
end
 
# features/step_definitions/something.rb
Given /^I provide my credentials on the checkout page$/
  go_to_checkout_page
  enter_email
  enter_password
end

While this is pretty good for fairly single-purpose pieces of functionality, when you want to re-use functions across many scenarios or even test suites, it is usually better practice to bundle them into modules. It is also better practice not to pollute the global namespace; fortunately cucumber provides for that:

# features/support/some_module.rb
module SomeModule
  def go_to_checkout_page
  end
 
  def enter_email
  end
 
  def enter_password
  end
end # SomeModule
 
# features/support/env.rb
require 'some_module'
World(SomeModule)

The step definitions remain the same, but putting functions into modules lets you organize them better and keep them from conflicting in the global namespace. Invoking World(SomeModule) extends the cucumber World object with the functions from the given module (or modules; multiple modules are possible). The World object is passed to each step definition as self, meaning all its functions are immediately available for the step definition to use.

Conclusion

Aside from the advantage that “push how down” provides the reader with more of the intent of a test scenario over it’s mechanics, fewer more powerful steps also means it’s easier to capture when workflows branch.

To deal with branches, examine your workflows for blocks of step definitions that always occur in this order; once identified, they’re prime candidates for folding into one, more complex step.

Additionally, instead of writing code directly in step definitions, think about re-usable pieces of functionality, and bundle them in modules that you extend World with.

  1. Small differences may be best captured in scenario outlines and example tables.
  2. Larger differences are fairly easily dealt with by using fewer, more complex steps.
  3. Assembling complex steps becomes easier when their common functionality is bundled in modules.

The only situation not captured in these guidelines are workflows of such complexity that they must necessarily include lots and lots of branches. If you find yourself confronted with a workflows of that complexity, it’s very likely that parts of the workflow can be separated out as Sub-Processes. Doing so will yield workflows more easily testable separately, and — if the above guidelines are adhered to — result in a number of Ruby modules you can re-use in your tests.


Reputation. Meet spriteCloud

Find out today why startups, SMBs, enterprises, brands, digital agencies, e-commerce, and mobile clients turn to spriteCloud to help improve their customer experiences. And their reputation. With complete range of QA services, we provide a full service that includes test planning, functional testing, test automation, performance testing, consultancy, mobile testing, and security testing. We even have a test lab — open to all our clients to use — with a full range of devices and platforms.

Discover how our process can boost your reputation.