Cucumber, washed and ready to eat for Friction-free ATDD/BDD
This is a work in progress - feedback welcome e-mail feedback to [email protected]
You can see our current list of intended features here: https://github.com/RiverGlide/CukeSalad/issues?labels=Feature
CukeSalad allows you to focus on the task at hand - expressing examples, the roles involved in those examples and what those roles can do with the product under development.
With CukeSalad you don't need to write step-definitions.
Of course, you still have to write some code - but only the code that expresses:
- the roles and the actions they can perform
- the tasks and the actions involved in completing that task
The terms "actions" and "tasks" above come from Task Analysis, as used in User Centred Design (UCD) of Human Computer Interfaces (HCI) a.k.a. User Experience (UX):
- Goal: What we’re trying to achieve which has one or more…
- Tasks: The high-level work-item that we complete to fulfil the goal, each having one or more…
- Actions: The specific steps or interactions we execute to complete the task.
More information about Goals, Tasks and Actions can be found in this blog post
Let's see how this works with a simple example...
gem install cukesalad
Create a new project called Calculator:
cukesalad Calculator
Or, if you have an existing cucumber project that you want to configure to use cukesalad, you can type:
cukesalad
In features/
, let's create our first feature file - a_place_to_start.feature
:
Feature: A Place To Start
As Callie, a calculating individual
I want to know when my calculator is on
So that I know when I can start calculating
Scenario: Let's Begin
Given I am a Calculating Individual
When I attempt to switch on the calculator
Then I should see the answer '0'
Let's take a moment to understand this scenario:
Scenario: Let's Begin
Given I am a <some role>
When I attempt to <do some task>
Then I should <ask some question> '<expected answer>'
To get this working, we don't need to write any steps. Instead, we describe tasks...
Explaining how to do a task is easy:
Create a new file, features/lib/tasks/switch_on_the_calculator.rb
Remember the step When I attempt to switch on the calculator
in_order_to "switch on the calculator" do
switch_on_the_calculator
end
Remember the step Then I should see the answer '0'
Now we need task/see_the_answer.rb
in_order_to "see the answer" do
look_at_the_display
end
Now we've explained the tasks, we need to define the role that performs them. In
this example, we need to explain how the CalculatingIndividual
role works...
Remember the step Given I am a Calculating Individual
?
We explain a role by creating a new file
called features/lib/roles/calculating_individual.rb
module CalculatingIndividual
def switch_on_the_calculator
@calculator = Calculator.new
end
def look_at_the_display
@calculator.display
end
end
You'll need a class called Calculator on the load path of course, but that's it.
From your project folder, run (note: '%' is our command prompt)
% cucumber
We now have our first passing Feature, without creating a single step definition!
Let's try another scenario...
Scenario Outline: Find the sum of two numbers
Given I am a Calculating Individual
And I was able to switch on the calculator
When I attempt to add: the number '10' and the number '10'
Then I should see the answer '20'
Notice that we've reused 'switch on the calculator'.
The new When step has a slightly different layout.
Let's examine that for a second... See below. Notice the ':' (colon) after <do something>
followed by the name-value pairs:
When I attempt to <do something>: <name> '<value>' <name> '<value>'
You can also use a ',' (comma) in situations where a colon wouldn't quite work:
When I attempt to <do something>, <name> '<value>' <name> '<value>'
<do something>
can be as many words as you like. You can have as many name-value pairs as you like.
For this to work we need a task called tasks/add.rb
that explains the individual actions required to complete the task:
in_order_to "add" do
enter @value_of(:the_number)
press :plus
enter @value_of(:and_the_number)
press :equals
end
Notice how the value_of
lines use symbols that correspond to the wording 'the number '10' to the number '10'
in the "When" step.
There is some 'syntactic sugar' that we can use to dress this up a little and make it read nicer... a simple attribute mapping (using Ruby 1.9.x syntax):
in_order_to "add", the_number: :first_number, to_the_number: :second_number do
enter the :first_number
press :plus
enter the :second_number
press :equals
end
All we've done is mapped :the_number
to :first_number
and :to_the_number
to :second_number
. There is a special method called "the" that allows you to reference the mapped values rather than the symbols derived from the scenario.
Now all we need to do is create the corresponding methods in calculating_individual.rb
.
module CalculatingIndividual
def switch_on_the_calculator
@calculator = Calculator.new
@operate_with = {
plus: :+,
minus: :-
}
end
def enter value
@calculator.enter value.to_i
end
def press next_operator
if next_operator == :equals
equals
else
@calculator.get_ready_to @operate_with[next_operator]
end
end
def equals
@calculator.equals
end
def look_at_the_display
@calculator.display
end
end
Of course you'll have to implement the calculator too
Now, you can run cucumber again:
% cucumber
There's no need to write step_definitions
...
Simply express the roles and the tasks in clear,
concise, easy to read classes.
If we want to know what things we can say, instead of trawling through a step-def ruby file, we can look in our tasks folder:
features/lib/default/tasks/
├── add.rb
├── calculate.rb
├── calculations.rb
├── see_the_answer.rb
├── subtract.rb
└── switch_on_the_calculator.rb
You can structure your tasks as you see fit. For example, as the project grows, it might end up looking like this:
features/lib/default/tasks/
├── all_purpose
| └── calculate.rb
├── arithmetic
| ├── add.rb
| ├── divide.rb
| ├── multiply.rb
| └── subtract.rb
├── extras
| ├── recall_from_memory.rb
| ├── store_in_memory.rb
| └── switch_on_the_calculator.rb
├── questions
| ├── see_the_answer.rb
| ├── see_the_following_indicators.rb
| └── switch_on_the_calculator.rb
├── trigonometry
| ├── sine.rb
| ├── cosine.rb
| └── tangent.rb
└── reference_material
└── calculations.rb
As our features describe the value of a calculator application and not its implementation, we have the opportunity to reuse them.
In the Calculator example, we create a new role in
./features/lib/alternative/roles/calculating_web_user.rb
, which we can swap
into our tests using a Cucumber profile defined in features/cucumber.yml
:
default --exclude features/lib/alternative/
alternative -r features/lib/alternative/ -r features/support/env.rb -r features/lib/default/tasks/
We can run our alternative configuration like so:
%cucumber --profile alternative
The Calculating Web User masquerades as the Calculating Individual from our previous example, and provides the same API, allowing us to reuse all of our existing features and tasks.
The alternative, ./lib/web_calculator.rb
, implementation is a simple Sinatra application,
which we drive with the Capybara web testing framework.
By writing a single new role class we're able to reuse all of our existing features,
tasks and even the Calculator
itself, which the web calculator delegates to in order to do its calculations.
After adding some more scenarios and tasks and an alternative "Calculating Individual" (see below for why), our Calculator directory structure currently looks like this...
├── cucumber.yml
├── features
│ ├── A_PlaceToStart.feature
│ ├── Addition.feature
│ ├── Complex_calculations.feature
│ ├── Subtraction.feature
│ ├── Typical_workflow.feature
│ ├── lib
│ │ ├── alternative
│ │ │ ├── roles
│ │ │ │ └── calculating_web_user.rb
│ │ │ └── tasks
│ │ └── default
│ │ ├── roles
│ │ │ └── calculating_individual.rb
│ │ └── tasks
│ │ ├── add.rb
│ │ ├── perform.rb
│ │ ├── see_the_answer.rb
│ │ ├── subtract.rb
│ │ └── switch_on_the_calculator.rb
│ └── support
│ └── env.rb
├── lib
│ ├── calculator.rb
│ └── web_calculator.rb
└── spec
├── calculator_spec.rb
└── web_calculator_spec.rb
Take a look around the examples and the code to see how it all works. We hope you enjoy reading as much as we enjoyed writing it.