Phil Trelford's Array
POKE 36879, 255

What makes a good step definition

September 5, 2011 03:51 by phil

Following on from last week’s What makes a good feature file article, this week it’s time to tackle what makes a good step definition. To automatically test that an application’s behaviour matches that defined by the scenarios in a feature file, a mapping is required. Step definitions provide that mapping from the lines in a feature file to the code that implements the feature.

Starting with a Given:

Given I have entered 0.1134 into the calculator

Implementations

Ruby step definition for use with Cucumber::

Given /I have entered (.*) into the calculator/ do |n|
  # Some Ruby code here
end

The code above specifies a regular expression used to match lines in the feature file and an anonymous function to execute.

C# attributed step definition compatible with both SpecFlow and TickSpec:

[Given(@"I have entered (.*) into the calculator")]
public void GivenIHaveEnteredNIntoTheCalculator(int n)
{
  // C# code here
}

Again a regular expression is specified, however this time the function is public and reflection is used for invocation.

F# attributed step definition compatible with TickSpec:

let [<Given>] ``I have entered (.*) into the calculator``(n:int) =
  // F# code here
}

F# allows white space and special charecters in method names enclosed with backticks. This removes the need for both an attribute and a function name.

TickSpec also supports anonymous functions as Ruby with Cucumber, i.e.:

Given "I have entered (.*) into the calculator" (fun [Int n] –>
  // F# code here
)

From a terseness point of view it looks like Ruby and F# the advantage for writing step definitions. But that’s only part of the story…

Scope 

By default in frameworks like Cucumber and SpecFlow step definitions are defined globally, so that they can be shared or reused. Global functions are usually fine for small programs, but for larger programs we usually group them into classes or modules for maintainability.

With SpecFlow and TickSpec it is possible to constrain the scope of a step definition or set of step definitions via a StepScope attribute:

[StepScope(Feature="Addition")]
public class AdditionSteps
{
  [Given(@"I have entered (.*) into the calculator")]
  public void GivenIHaveEnteredNIntoTheCalculator(int n)
  {
    // C# code here
  }
}

Here the step definition methods in the AdditionSteps class are only matched against the Addition feature. This allows the wording in the feature file to evolve without affecting other feature files, and it allows the actions in the methods to vary too. It also means that it is easy to find the step definitions that map to a feature file in one place.

Using a class level step definition scope is quite similar conceptually to using a View Model within the MVVM pattern, where the View Model is responsible for mapping from view to model. It adds a layer of indirection over binding directly to the model, but allows the application to evolve more easily over time.

Note: TickSpec can apply scope to both Step bindings and Event bindings like BeforeScenario and AfterScenario. There is currently was an open bug on this in SpecFlow (since fixed).

Fluent interfaces

So far the step definition examples have included a “code here” comment. This is where we arrange, act and assert. In the Given steps the preconditions are arranged. The When steps execute the actions. Finally the Then steps assert the outcome. This is almost identical to the steps you’d take in TDD for a unit test, except the parts are spread across multiple methods.

Note: The state for a scenario can also be initialized in a method marked with BeforeScenario attribute

For the arrange and act steps, DSL approaches like fluent interfaces can be useful for making step definitions more readable and maintainable.

In the arrange steps using the builder pattern is useful to build up the initial state:

public void constructPizza()
{
  pizzaBuilder.createNewPizzaProduct();
  pizzaBuilder.buildDough();
  pizzaBuilder.buildSauce();
  pizzaBuilder.buildTopping();
}

In the act steps a fluent assertion API is useful, for example the FsUnit F# library:

1 |> should equal 1
anArray |> should haveLength 4
anObj |> should not (be Null)

Summary

Using fluent interfaces inside the step definition functions can make them more readable and maintainable. For larger projects constraining the scope of step definitions helps make them more maintainable. Finally step definitions can sometimes be written more concisely in languages like Ruby and F#.


Tags:
Categories: BDD | .Net | C# | F# | Ruby
Actions: E-mail | Permalink | Comments (3) | Comment RSSRSS comment feed

Comments

September 5. 2011 04:04

jonas

Just as a short pointer:

Scoped Bindings are considered an antipattern in the cucumber community:
github.com/.../Feature-Coupled-Steps-%28Antipattern%29

For SpecFlow there are discussions if scoped bindings should be supported:
http://aaftt.agilealliance.org:8080/display/AAFTT/The+Future+of+Cucumber+on+.NET

jonas

September 5. 2011 05:24

Phil

Jonas,

Thanks for the links. I guess with everything, the usefulness of scope bindings is going to be context specific.
On a number of projects I've worked on, not applying scope has led to the step definitions becoming hard to maintain and having changes for 1 feature causing regressions in another.
Equally on the Cellz project:
http://cellz.codeplex.com/wikipage?title=Automated%20Acceptance%20Tests&referringTitle=Home
I've used a set of step definitions against 3 feature files.

Cheers,
Phil

Phil

October 26. 2011 09:42

Phil

Looks like not everybody in the Ruby/BDD community thinks feature coupled steps are an anti-pattern.

The Ruby Spinach project (http://codegram.github.com/spinach/) features encapsulation and modularity of your step definitions.
Some of its design goals:
• Step maintanability: since features map to their own classes, their steps are just methods of that class. This encourages step encapsulation.
• Step reusability: In case you want to reuse steps across features, you can always wrap those in plain ol' Ruby modules.

P.S. information via Jonas  

Phil

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading