Composable Features and Tables

June 27, 2011

Recently I've been commenting on the this blog article. I think it would be better to gather my thoughts here.

The Problem With Tables

I consider tables in features a smell. They are a warning that maybe something is not quite right, and that there is some more work to do. Lets consider the following from the watirmelon article.

Feature: Beautiful Tea Shipping Costs

    - Australian customers pay GST
    - Overseas customers don’t pay GST
    - Australian customers get free shipping for orders $100 and above
    - Overseas customers all pay the same shipping rate regardless of order size

    Scenario: Calculate GST status and shipping rate
      Given the customer is from <customer’s country>
      When the customer’s order totals <order total>
      Then the customer <pays GST>
      And they are charged <shipping rate>
    Examples:
    | customer’s country | pays GST | order total | shipping rate     |
    | Australia          | Must     | $99.99      | Standard Domestic |
    | Australia          | Must     | $100.00     | Free              |
    | New Zealand        | Must not | $99.99      | Standard Domestic |
    | New Zealand        | Must not | $99.99      | Standard Domestic |
    | New Zealand        | Must not | $100.00     | Standard Domestic |
    | Zimbabwe           | Must not | $100.00     | Standard Domestic |

At first view this table seems like an elegant summary of 'Beautiful Tea Shipping Costs', but lets examine this a little bit further.

If we expand the table into individual features we can get something like

Feature: Beautiful Tea Shipping Costs

    Scenario: Australian customers pay GST
    Given the customer is from Australia
    When the customer buys something
    Then the customer pays GST

    Scenario: Overseas customers don’t GST
    Given the customer is from overseas
    When the customer buys something
    Then the customer does not pay GST

    Scenario: Australian customers who buy lots can get free shipping
    Given the customer is from Australia
    When the customer buys lots
    Then the customer gets free shipping

    Scenario: Australian customers who buy bugger all can't get free shipping
    Given the customer is from Australia
    When the customer buys bugger all
    Then the customer pays for shipping

    Scenario: Overseas customers can’t get free shipping
    Given the customer is from overseas
    When the customer buys bugger all
    Then the customer pays for shipping

    Scenario: Overseas customers can’t get free shipping, even if they buy lots
    Given the customer is from overseas
    When the customer buys lots
    Then the customer pays for shipping

There are a few things to notice here:

  1. There is no need for comments. The scenarios specify the rules
  2. There is alot of repetition

Lets address the second point by refactoring

Feature: Shipping for Overseas Customers
      As a site owner
      I always want overseas customers to pay for shipping
      Because shipping overseas is to expensive to ever do free

    Background:
      Given the customer is from overseas

    Scenario: Overseas customers don’t pay GST
      When the customer buys something
      Then the customer does not pay GST

    Scenario: Buying lots
      When the customer buys lots
      Then the customer pays for shipping

    Scenario: Buying bugger all
      When the customer buys bugger all
      Then the customer pays for shipping

Here we have split the feature up by differentiating between domestic and overseas

Feature: Shipping for Domestic Customers
      As a site owner
      I always want domestic customers to get free shipping
      Because free shipping increases profitability by encouraging
      more purchases.

    Background:
    Given a domestic customer

    Scenario: Domestic customers pay GST
      When the customer buys something
      Then the customer pays GST

    Scenario: Buying lots
      When the customer buys lots
      Then the customer pays for shipping

    Scenario: Buying bugger all
      When the customer buys bugger all
      Then the customer gets free shipping

We could have split the feature by amount purchased or even by GST, which one we choose is very much dependent on context.

Now we have two small very simple features that cover all the ground that the table feature does. There are a few points that we are now in a position to look at in greater detail

Where has the order total gone

The original table specified the threshold at which shipping was set which is $100. Implementing this in the application would almost certainly be done by using a constant - shipping_threshold and then basing all the calculations on this constant.

So what happens when the value of the constant changes?

  • The features will fail - even though the application is working fine

The price of changing this constant is that the feature has to be rewritten, but actually nothing is wrong with the application. Now this is a real pain for the site owners. Doing things like having a free shipping promotion at Christmas now becomes something that requires development work and rewriting features, or worse that we actually run with failing features.

Questioning the Business Rules

The most important part of writing a feature file, and the part that most people skip over is the first three lines of the feature. What happens if we compare these from our two features

As a site owner
I always want domestic customers to get free shipping
Because free shipping increases profitability by encouraging
more purchases.

As a site owner
I always want overseas customers to pay for shipping
Because shipping overseas is to expensive to ever do free

Lets question these justifications. If free shipping always encourages more purchases, then we would like to do free shipping for overseas customers. However we've said its always too expensive. But is it? Are there some situations where it would be profitable to free ship to overseas e.g. a large order that doesn't weigh much; an order for very expensive goods. Separating the feature into these components give us the ability to ask these questions before we do any work

Implementing the Step Definitions

Implementing the step definitions for the table is going to be difficult. We have to process a table of hashes, use custom processing for 'Pays GST' and convert the numeric values in the table. All of this is error prone.

Consider the refactored steps. There are no tables to deal with, no arguments to process. Each step is much easier to implement, is reusable and is reused. This means we can go faster. Just by changing the language of our feature we have reduced the cost of implementing it.

Summary

If you come across a table in a feature, or if you feel the need to write a table STOP and have a think. Its likely that there is a simpler approach which will achieve what you want with less work and easier to read features