Writing Software not Code with Cucumber

Post on 19-Jan-2015

39,542 views 1 download

Tags:

description

Describes Outside-In development and Behvaiour Driven Development. Illustrates basic Cucumber usage within a Rails app and then goes over more advanced topics such as JS as web services.

transcript

With

Ben Mabey

Writing Software not code

With

Ben Mabey

Writing Software not code

With

Ben Mabey

Writing Software not code

Behaviour Driven Development

?

Tweet in the blanks...

"most software projects are like _ _ _ _ _ _ _ _"

#rubyhoedown #cucumber

"most software projects are like _ _ _ _ _ _ _ _"

#rubyhoedown #cucumber

So... why are software projects like “The Homer”?

Feature Devotion

TextPlacing emphasis on features instead of

overall outcome

Shingeo Shingo of Toyota says...

"Inspection to find defects is waste."

"Inspection to prevent defects is

essential."

"Inspection to find defects is waste."

56% of all bugs are introduced in requirements. (CHAOS Report)

Root Cause Analysis

Popping the Why Stack...

Protect Revenue

Increase Revenue

Manage Cost

Feature: title

In order to [Business Value]As a [Role]I want to [Some Action] (feature)

* not executed* documentation value* variant of contextra* business value up front

There is no template.What is important to have in narrative:

* business value * stakeholder role * user role * action to be taken by user

<rant>

With

Ben Mabey

Writing Software not code

Behaviour Driven Development

!= BDD

!= BDD

RSpec != BDD

RSpec != BDD

“All of these tools are great... but, in the end, tools are tools. While RSpec and Cucumber are optimized for BDD, using them

doesn’t automatically mean you’re doing BDD"

The RSpec Book

BDD is a mindset

not a tool set

</rant>

Feature: title

In order to [Business Value]As a [Role]I want to [Some Action] (feature)

* not executed* documentation value* variant of contextra* business value up front

Scenario: titleGiven [Context]When I do [Action]Then I should see [Outcome]

Scenario: titleGiven [Context]And [More Context]When I do [Action]And [Other Action]Then I should see [Outcome]But I should not see [Outcome]

project_root/| `-- features

project_root/| `-- features |-- awesomeness.feature |-- greatest_ever.feature

project_root/| `-- features |-- awesomeness.feature |-- greatest_ever.feature `-- support |-- env.rb `-- other_helpers.rb

project_root/| `-- features |-- awesomeness.feature |-- greatest_ever.feature `-- support |-- env.rb `-- other_helpers.rb |-- step_definitions | |-- domain_concept_A.rb | `-- domain_concept_B.rb

Step

Given a widget

Step

Given a widgetGiven /^a widget$/ do #codes go hereend

Definition

Step

Given a widgetGiven /^a widget$/ do #codes go hereend

Definition

Step Mother

Step

Given a widgetGiven /^a widget$/ do #codes go hereend

Definition

Step Mother

a.featureb.feature

a.featureb.feature

a.featureb.feature

a.featureb.feature 28+

Languages

a.featureb.feature

x_steps.rby_steps.rb

28+ Languages

a.featureb.feature

x_steps.rby_steps.rb

RSpec, Test::Unit, etc

28+ Languages

a.featureb.feature

x_steps.rby_steps.rb

Your Code

RSpec, Test::Unit, etc

28+ Languages

Not Just for Rails

Outside-In

Write Scenarios

Steps are pending

Write Step Definition

Go Down A Gear

RSpec, TestUnit, etc

Write Code Example(Unit Test)

Make Example Pass

REFACTOR!!

Where Are we?

Continue until...

REFACTORand

REPEAT

features/manage_my_wishes.feature

Feature: manage my wishes

In order to get more stuff As a greedy person I want to manage my wish list for my family members to view @proposed Scenario: add wish

@proposed Scenario: remove wish

@proposed Scenario: tweet wish

features/manage_my_wishes.feature

Feature: manage my wishes

In order to get more stuff As a greedy person I want to manage my wish list for my family members to view

@wip Scenario: add wish Given I am logged in When I make a "New car" wish Then "New car" should appear on my wish list

@proposedScenario: remove wish

@proposedScenario: tweet wish

Work In Progress

Workflow

Workflow

git branch -b add_wish_tracker#

Workflow

git branch -b add_wish_tracker#Tag Scenario or Feature with @wip

Workflow

git branch -b add_wish_tracker#Tag Scenario or Feature with @wip

cucumber --wip --tags @wip

Workflow

git branch -b add_wish_tracker#Tag Scenario or Feature with @wip

cucumber --wip --tags @wipDevelop it Outside-In

Workflow

git branch -b add_wish_tracker#Tag Scenario or Feature with @wip

cucumber --wip --tags @wipDevelop it Outside-In

git rebase ---interactive; git merge

Workflow

git branch -b add_wish_tracker#Tag Scenario or Feature with @wip

cucumber --wip --tags @wipDevelop it Outside-In

git rebase ---interactive; git mergeRepeat!

@wip on master?

$ rake -T cucumber

@wip on master?

$ rake -T cucumberrake cucumber:ok OR rake cucumber

@wip on master?

$ rake -T cucumberrake cucumber:ok OR rake cucumber

cucumber --tags ~@wip --strict

@wip on master?

$ rake -T cucumberrake cucumber:ok OR rake cucumber

cucumber --tags ~@wip --strict

@wip on master?Tag Exclusion

@wip on master?$ rake -T cucumber

@wip on master?$ rake -T cucumberrake cucumber:wip

cucumber --tags @wip:2 --wip

@wip on master?$ rake -T cucumberrake cucumber:wip

cucumber --tags @wip:2 --wip

@wip on master?$ rake -T cucumberrake cucumber:wip

Limit tags in flow

cucumber --tags @wip:2 --wip

@wip on master?$ rake -T cucumberrake cucumber:wip

Limit tags in flow

Expect failure - Success == Failure

@wip on master?$ rake -T cucumberrake cucumber:all

Runs both ok and wip -- great for CI

features/manage_my_wishes.feature

Feature: manage my wishes

In order to get more stuff As a greedy person I want to manage my wish list for my family members to view

@wip Scenario: add wish Given I am logged in When I make a "New car" wish Then "New car" should appear on my wish list

@proposedScenario: remove wish

@proposedScenario: tweet wish

Line # of scenario

Look Ma! backtraces!Given I am logged in #features/manage_my_wishes.feature:8

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

end

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

end

Test Data Builder / Object Mother

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

end

spec/fixjour_builders.rb

Fixjour do define_builder(User) do |klass, overrides| klass.new( :email => "user#{counter(:user)}@email.com", :password => 'password', :password_confirmation => 'password' ) endend

Fixture Replacement, Fixjour, Factory Girl, etc

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

end

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend

Webrat / Awesomeness

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend

Webrat / Awesomeness

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend

features/support/env.rb

require 'webrat'

Webrat.configure do |config| config.mode = :railsend

Webrat / Awesomeness

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend

features/support/env.rb

require 'webrat'

Webrat.configure do |config| config.mode = :railsend

Adapter

Webrat / Awesomeness

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend

features/step_definitions/webrat_steps.rb

When /^I press "(.*)"$/ do |button| click_button(button)end

When /^I follow "(.*)"$/ do |link| click_link(link)end

When /^I fill in "(.*)" with "(.*)"$/ do |field, value| fill_in(field, :with => value) end

When /^I select "(.*)" from "(.*)"$/ do |value, field| select(value, :from => field) end

# Use this step in conjunction with Rail's datetime_select helper. For example:# When I select "December 25, 2008 10:00" as the date and time When /^I select "(.*)" as the date and time$/ do |time| select_datetime(time)end

# Use this step when using multiple datetime_select helpers on a page or # you want to specify which datetime to select. Given the following view:# <%= f.label :preferred %><br /># <%= f.datetime_select :preferred %># <%= f.label :alternative %><br /># <%= f.datetime_select :alternative %># The following steps would fill out the form:# When I select "November 23, 2004 11:20" as the "Preferred" data and time# And I select "November 25, 2004 10:30" as the "Alternative" data and timeWhen /^I select "(.*)" as the "(.*)" date and time$/ do |datetime, datetime_label| select_datetime(datetime, :from => datetime_label)end

# Use this step in conjunction with Rail's time_select helper. For example:# When I select "2:20PM" as the time# Note: Rail's default time helper provides 24-hour time-- not 12 hour time. Webrat# will convert the 2:20PM to 14:20 and then select it. When /^I select "(.*)" as the time$/ do |time| select_time(time)end

# Use this step when using multiple time_select helpers on a page or you want to# specify the name of the time on the form. For example:# When I select "7:30AM" as the "Gym" timeWhen /^I select "(.*)" as the "(.*)" time$/ do |time, time_label| select_time(time, :from => time_label)end

# Use this step in conjunction with Rail's date_select helper. For example:# When I select "February 20, 1981" as the dateWhen /^I select "(.*)" as the date$/ do |date| select_date(date)end

# Use this step when using multiple date_select helpers on one page or# you want to specify the name of the date on the form. For example:# When I select "April 26, 1982" as the "Date of Birth" dateWhen /^I select "(.*)" as the "(.*)" date$/ do |date, date_label| select_date(date, :from => date_label)end

When /^I check "(.*)"$/ do |field| check(field) end

When /^I uncheck "(.*)"$/ do |field| uncheck(field) end

When /^I choose "(.*)"$/ do |field| choose(field)end

When /^I attach the file at "(.*)" to "(.*)" $/ do |path, field| attach_file(field, path)end

Then /^I should see "(.*)"$/ do |text| response.should contain(text)end

Then /^I should not see "(.*)"$/ do |text| response.should_not contain(text)end

Then /^the "(.*)" checkbox should be checked$/ do |label| field_labeled(label).should be_checkedend

Webrat / Awesomeness

20+ Steps Out-of-box

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_button

end

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do

# make sure we have actually logged in- so we fail fast if not session[:user_id].should == @current_user.idend

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do

# make sure we have actually logged in- so we fail fast if not controller.current_user.should == @current_userend

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_button session[:user_id].should == @current_user.id

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_button

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do

# make sure we have actually logged in- so we fail fast if not session[:user_id].should == @current_user.id controller.current_user.should == @current_user response.should contain("Signed in successfully")end

Specify outcome, not implementation.

features/step_definitions/user_steps.rb

Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)

visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_button # make sure we have actually logged in- so we fail fast if not response.should contain("Signed in successfully")end

No route matches “/sessions/create” with{:method=>:post} (ActionController::RoutingError)

I’m going to cheat...

I’m going to cheat...$ gem install thoughtbot-clearance$ ./script generate clearance$ ./script generate clearance_features

features/step_definitions/wish_steps.rb

When /^I make a "(.+)" wish$/ do |wish| end

Then /^(.+) should appear on my wish list$/ do |wish| end

features/step_definitions/wish_steps.rb

When /^I make a "(.+)" wish$/ do |wish| end

Then /^(.+) should appear on my wish list$/ do |wish| end

Regexp Capture -> Yielded Variable

features/step_definitions/wish_steps.rb

When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend

Then /^(.+) should appear on my wish list$/ do |wish| end

features/step_definitions/wish_steps.rb

Then /^(.+) should appear on my wish list$/ do |wish| response.should contain("Your wish has been added!") response.should contain(wish)end

When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend

features/step_definitions/wish_steps.rb

When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend

Then /^(.+) should appear on my wish list$/ do |wish| response.should contain("Your wish has been added!") response.should contain(wish)end

No route matches “/wishes” with{:method=>:get} (ActionController::RoutingError)

config/routes.rb

ActionController::Routing::Routes.draw do |map| map.resources :wishes

config/routes.rb

ActionController::Routing::Routes.draw do |map| map.resources :wishes

When I make a “New car” wishuninitialized constant WishesController (NameError)

config/routes.rb

ActionController::Routing::Routes.draw do |map| map.resources :wishes

$./script generate rspec_controller new create

config/routes.rb

ActionController::Routing::Routes.draw do |map| map.resources :wishes

When I make a “New car” wishCould not find link with text or title orid “Make a wish” (Webrat::NotFoundError)

app/views/wishes/index.html.erb

<%= link_to "Make a wish", new_wish_path %>

app/views/wishes/index.html.erb

<%= link_to "Make a wish", new_wish_path %>

When I make a “New car” wish Could not find field: “Wish” (Webrat::NotFoundError)

features/step_definitions/wish_steps.rb

When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend

features/step_definitions/wish_steps.rb

When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend

app/views/wishes/new.html.erb

<% form_for :wish do |f| %> <%= f.label :name, "Wish" %> <%= f.text_field :name %> <%= submit_tag "Make the wish!" %><% end %>

fill_in "Wish", :with => wish

<%= f.label :name, "Wish" %> <%= f.text_field :name %>

features/step_definitions/wish_steps.rb

When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend

app/views/wishes/new.html.erb

<% form_for :wish do |f| %> <%= submit_tag "Make the wish!" %><% end %>

Location Strategy FTW!

View

Controller

spec/controllers/wishes_controller_spec.rb

describe WishesController do describe "POST / (#create)" do

endend

spec/controllers/wishes_controller_spec.rb

describe WishesController do describe "POST / (#create)" do it "creates a new wish for the user with the params" do user = mock_model(User, :wishes => mock("wishes association")) controller.stub!(:current_user).and_return(user) user.wishes.should_receive(:create).with(wish_params)

post :create, 'wish' => {'name' => 'Dog'} end endend

app/controllers/wishes_controller.rb

class WishesController < ApplicationController

def create current_user.wishes.create(params['wish']) end

end

spec/controllers/wishes_controller_spec.rb

describe WishesController do describe "POST / (#create)" do before(:each) do .....

endend

spec/controllers/wishes_controller_spec.rb

it "redirects the user to their wish list" do do_post response.should redirect_to(wishes_path) end

app/controllers/wishes_controller.rb

def create current_user.wishes.create(params['wish']) redirect_to :action => :index end

View

Controller

Model

app/controllers/wishes_controller.rb

def create current_user.wishes.create(params['wish']) redirect_to :action => :index end

When I make a “New car” wishundefined method `wishes` for #<User:0x268e898> (NoMethodError)

app/controllers/wishes_controller.rb

def create current_user.wishes.create(params['wish']) redirect_to :action => :index end

$./script generate rspec_model wish name:string user_id:integer

app/models/wish.rb

class Wish < ActiveRecord::Base belongs_to :userend

app/models/user.rb

class User < ActiveRecord::Base include Clearance::App::Models::User has_many :wishesend

app/models/wish.rb

class Wish < ActiveRecord::Base belongs_to :userend

app/models/user.rb

class User < ActiveRecord::Base include Clearance::App::Models::User has_many :wishesend

When I make a “New car” wishThen “New car” should appear on my wishexpected the following element’s content to include “Your wish has been added!”

spec/controllers/wishes_controller_spec.rb

it "notifies the user of creation via the flash" do do_post flash[:success].should == "Your wish has been added!"end

current_user.wishes.create(params['wish']) redirect_to :action => :index

app/controllers/wishes_controller.rb

def create flash[:success] = "Your wish has been added!" end

spec/controllers/wishes_controller_spec.rb

it "notifies the user of creation via the flash" do do_post flash[:success].should == "Your wish has been added!"end

current_user.wishes.create(params['wish']) redirect_to :action => :index

app/controllers/wishes_controller.rb

def create flash[:success] = "Your wish has been added!" end

spec/controllers/wishes_controller_spec.rb

it "should notifies the user of creation via the flash" do do_post flash[:success].should == "Your wish has been added!"end

Then “New car” should appear on my wishexpected the following element’s content to include “New car”

app/views/wishes/index.html.erb

<ul><% @wishes.each do |wish| %> <li><%= wish.name %></li><% end %></ul>

spec/controllers/wishes_controller_spec.rb

describe "GET / (#index)" do def do_get get :index end

it "assigns the user's wishes to the view" do do_get assigns[:wishes].should == @current_user.wishes end

end

app/controllers/wishes_controller.rb

def index @wishes = current_user.wishes end

FAQ

How do I test JS and

AJAX?

Slow

Fast

Slow

Fast

Integrated

Isolated

Slow

Fast

Integrated

Isolated

Slow

Fast

Integrated

Isolated

Slow

Fast

Integrated

Isolated

Slow

Fast

Integrated

Isolated

Slow

Fast

Slow

Fast Joyful

Slow

Fast

Painful

Joyful

Slow

Fast

Painful

Joyful

Celerity

Celerity

CelerityHtmlUnit

CelerityHtmlUnit

CelerityHtmlUnit

CelerityHtmlUnit

require "rubygems"require "celerity"

browser = Celerity::Browser.new

browser.goto('http://www.google.com')browser.text_field(:name, 'q').value = 'Celerity'browser.button(:name, 'btnG').click

puts "yay" if browser.text.include? 'celerity.rubyforge.org'

What if I use MRI?

require "rubygems"require "culerity"

culerity_server = Culerity::run_server

browser = Culerity::RemoteBrowserProxy.new(culerity_server)browser.goto('http://www.google.com')browser.text_field(:name, 'q').value = 'Celerity'browser.button(:name, 'btnG').click

puts "yay" if browser.text.include? 'celerity.rubyforge.org'

CodeNote

http://github.com/bmabey/codenote

Feature: CLI Server In order to save me time and headaches As a presenter of code I create a presentation in plaintext a'la Slidedown (from Pat Nakajima) and have CodeNote serve it up for me

Scenario: basic presentation loading and viewing Given that the codenote server is not running And a file named "presentation.md" with: """ !TITLE My Presentation !PRESENTER Ben Mabey # This is the title slide !SLIDE # This is second slide... """ When I run "codenote_load presentation.md" And I run "codenote" And I visit the servers address

For example of how to test CLI tools take a look at CodeNote on github.

RSpec and Cucumber also have good examples of how to do this.

Feature: Twitter Quiz In order to encourage audience participation where 90% of the audience is hacking on laptops As a presenter I want audience members To answer certain questions via twitter

Feature: Twitter Quiz In order to encourage audience participation where 90% of the audience is hacking on laptops As a presenter I want audience members To answer certain questions via twitter

@proposed Scenario: waiting for an answer

@proposed Scenario: winner is displayed

@proposed Scenario: fail whale

@proposed Scenario: network timeout

Feature: Twitter Quiz In order to encourage audience participation where 90% of the audience is hacking on laptops As a presenter I want audience members To answer certain questions via twitter

@wip Scenario: waiting for an answer

@wipScenario: waiting for an answer Given the following presentation """ !TITLE American History !PRESENTER David McCullough # Wanna win a prize? ### You'll have to answer a question... ### in a tweet! First correct tweet wins! !SLIDE # Who shot Alexander Hamilton? ## You must use #free_stuff in your tweet. !DYNAMIC-SLIDE TwitterQuiz '#free_stuff "aaron burr"' !SLIDE Okay, that was fun. Lets actually start now. """

!DYNAMIC-SLIDE TwitterQuiz '#free_stuff "aaron burr"'

@wipScenario: waiting for an answer Given the following presentation """ !TITLE American History !PRESENTER David McCullough # Wanna win a prize? ### You'll have to answer a question... ### in a tweet! First correct tweet wins! !SLIDE # Who shot Alexander Hamilton? ## You must use #free_stuff in your tweet. !SLIDE Okay, that was fun. Lets actually start now. """

@wipScenario: waiting for an answer Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search When the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see "And the winner is..." And I should see an ajax spinner

Given /the following presentation$/ do |presentation| end

Given the following presentation """ blah, blah """

Given /the following presentation$/ do |presentation| end

Given the following presentation """ blah, blah """

Yields the multi-line string

CodeNote::PresentationLoader.setup(presentation)

Given /the following presentation$/ do |presentation| end

Given the following presentation """ blah, blah """

RSpec Cycle

And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search

FAQ

How do I test webservices?

http://github.com/chrisk/fakeweb

page = `curl -is http://www.google.com/`FakeWeb.register_uri(:get, "http://www.google.com/",

:response => page)

Net::HTTP.get(URI.parse("http://www.google.com/")) # => Full response, including headers

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| end

And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

Helpers

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end

def canned_response_for(query) .... return file_path end

Helpers

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end

def canned_response_for(query) .... return file_path end

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end

def canned_response_for(query) .... return file_path end

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end

def canned_response_for(query) .... return file_path end

“Every time you monkeypatch Object, a kitten dies.”

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end

def canned_response_for(query) .... return file_path end

end

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end

def canned_response_for(query) .... return file_path end

end

World(TwitterHelpers)

Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end

module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end

def canned_response_for(query) .... return file_path end

end

World(TwitterHelpers)

When the presenter goes to the 3rd slide

When the presenter goes to the 3rd slide

When /the presenter goes to the (\d+)(?:st|nd|rd|th) slide$/ do |slide_number|

presenter_browser.goto path('/') (slide_number.to_i - 1).times do presenter_browser.link(:text, "Next").click endend

When the presenter goes to the 3rd slide

When /the presenter goes to the (\d+)(?:st|nd|rd|th) slide$/ do |slide_number|

presenter_browser.goto path('/') (slide_number.to_i - 1).times do presenter_browser.link(:text, "Next").click endend

Presenter has own browser,multiple sessions!

And I go to the 3rd slide Then I should see "And the winner is..."

And I go to the 3rd slide Then I should see "And the winner is..."

When /I go to the (\d+)(?:st|nd|rd|th) slide$/ do |slide_number| browser.goto path("/slides/#{slide_number}")end

And I go to the 3rd slide Then I should see "And the winner is..."

When /I go to the (\d+)(?:st|nd|rd|th) slide$/ do |slide_number| browser.goto path("/slides/#{slide_number}")end

Then /I should see "(["]*)"$/ do |text| browser.should contain(text)end

And I should see an ajax spinner

And I should see an ajax spinner

Then /I should see an ajax spinner$/ do browser.image(:id, 'spinner').exists?.should be_trueend

Brief RSpec cycle?

Scenario: waiting for an answer Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search When the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see "And the winner is..." And I should see an ajax spinner

Feature: Twitter Quiz In order to encourage audience participation where 90% of the audience is hacking on laptops As a presenter I want audience members To answer certain questions via twitter

Scenario: waiting for an answer ..... @wip Scenario: winner is displayed

@wipScenario: winner is displayed Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search

@wipScenario: winner is displayed Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search

Duplication of context!

Feature: Twitter Quiz ... Background: A presentation with a Twitter Quiz

Given the following presentation """ blah, blah """ And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search

Scenario: waiting for an answer When the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see "And the winner is..." And I should see an ajax spinner

@wip Scenario: winner is displayed

Extract to ‘Background’

@wipScenario: winner is displayed When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search | From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago | And the presenter goes to the 3rd slide And I go to the 3rd slide

Then I should see @jefferson's tweet along with his avatar

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table|

end

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table|

end

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

Cucumber::AST::Table

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))

end

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))

end

Umm... that won’t work.

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))

end

What I would really like is atest data builder/factory for

twitter searches...

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

http://github.com/bmabey/faketwitterrequire 'faketwitter'

FakeTwitter.register_search("#cheese", {:results => [{:text => "#cheese is good"}]})

require 'twitter_search'TwitterSearch::Client.new('').query('#cheese')=> [#<TwitterSearch::Tweet:0x196cef8 @id=1, @text="#cheese is good", @created_at="Fri, 21 Aug 2009 09:31:27 +0000", @to_user_id=nil, @from_user_id=1, @to_user=nil, @source="<a href="http://twitter.com/">web</a>", @iso_language_code="en", @from_user="jojo", @language="en", @profile_image_url="http://s3.amazonaws.com/twitter_production/profile_images/1/photo.jpg">]

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeTwitter.register_search(query, { :results => tweet_table.hashes})

end

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeTwitter.register_search(query, { :results => tweet_table.hashes})

end

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

Our headers and columnsaren’t compatible with API.

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| tweet_table.map_headers! do |header| header.downcase.gsub(' ','_') end

FakeTwitter.register_search(query, {:results => tweet_table.hashes})

end

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| tweet_table.map_headers! do |header| header.downcase.gsub(' ','_') end

tweet_table.map_column!('created_at') do |relative_time| interpret_time(relative_time) end

FakeTwitter.register_search(query, {:results => tweet_table.hashes})

end

When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search

| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |

@wipScenario: winner is displayed When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search | From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago | And the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see @jefferson's tweet along with his avatar

Then %r{I should see @([']+)'s tweet along with (?:his|her) avatar$} do |user|

tweet = FakeTwitter.tweets_from(user).first browser.should contain(tweet['text'], :wait => 10) browser.should have_image(:src, tweet['profile_image_url'])end

Timeout

Then %r{I should see @([']+)'s tweet along with (?:his|her) avatar$} do |user|

tweet = FakeTwitter.tweets_from(user).first browser.should contain(tweet['text'], :wait => 10) browser.should have_image(:src, tweet['profile_image_url'])end

Spec::Matchers.define :contain do |text, options| match do |browser| options[:wait] ||= 0 browser.wait_until(options[:wait]) do browser.text.include?(text) end endend

Then %r{I should see @([']+)'s tweet along with (?:his|her) avatar$} do |user|

tweet = FakeTwitter.tweets_from(user).first browser.should contain(tweet['text'], :wait => 10) browser.should have_image(:src, tweet['profile_image_url'])end

Spec::Matchers.define :contain do |text, options| match do |browser| options[:wait] ||= 0 browser.wait_until(options[:wait]) do browser.text.include?(text) end endend

Keep trying after sleepinguntil it times out

RSpec Cycle

Scenario: winner is displayed When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search | From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago | And the presenter goes to the 3rd slide And I go to the 3rd slide

Then I should see @jefferson's tweet along with his avatar

Demo!

More tricks...

Scenario: view members list Given the following wishes exist | Wish | Family Member | | Laptop | Thomas | | Nintendo Wii | Candace | | CHEEZBURGER | FuzzBuzz | When I view the wish list for "Candace"

Then I should see the following wishes | Wish | | Nintendo Wii |

Given the following wishes exist | Wish | Family Member | | Laptop | Thomas | | Nintendo Wii | Candace | | CHEEZBURGER | FuzzBuzz |

features/step_definitions/wish_steps.rb

Given /^the following wishes exist$/ do |table|

endend

| Wish | Family Member | features/step_definitions/wish_steps.rb

Given /^the following wishes exist$/ do |table| table.hashes.each do |row| member = User.find_by_name(row["Family Member"]) || create_user(:name => row["Family Member"])

member.wishes.create!(:name => row["Wish"]) endend

Given the following wishes exist | Wish | Family Member | | Laptop | Thomas | | Nintendo Wii | Candace | | CHEEZBURGER | FuzzBuzz |

Table Diffinghttp://wiki.github.com/aslakhellesoy/cucumber/multiline-step-arguments

Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers

Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen

Scenarios: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |

<input_1> <input_2> <button> <output>

| input_1 | input_2 | button | output |

Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers

Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen

Scenarios: addition | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | Scenarios: subtraction | 0 | 40 | minus | -40 |

Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers

Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen

Scenarios: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |

<input_1> <input_2> <button> <output>

| 20 | 30 | add | 50 |

<input_1> <input_2> <button> <output>

| 2 | 5 | add | 7 |

Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers

Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen

Scenarios: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |

<input_1> <input_2> <button> <output>

| 0 | 40 | add | 40 |

Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers

Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen

Scenarios: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |

When /^I view the wish list for "(.+)"$/ do |user_name| Given "I am logged in" visit "/wishes/#{user_name}"end

Steps Within Steps

When /^I view the wish list for "(.+)"$/ do |user_name| visit "/wishes/#{user_name}"end

Steps Within Steps Given "I am logged in"

HooksBefore doend

After do |scenario|end

World doend

World(MyModule)World(HerModule)

Tagged HooksBefore('@im_special', '@me_too') do @icecream = trueend

@me_tooFeature: Lorem Scenario: Ipsum Scenario: Dolor

Feature: Sit @im_special Scenario: Amet Scenario: Consec

Spork

http://github.com/timcharper/spork

Sick of slow loading times? Spork will load your main environment once. It then runs a DRB server so cucumber (or RSpec) can run against it with the --drb flag. For each test run Spork forks a child process to run them in a clean memory state. So.. it is a DRb server that forks.. hence Spork. :)

Drinking the Cucumber Kool-Aid?

Integration tests are a scam

http://www.jbrains.ca/permalink/239

J. B. Rainsberger

Obviously, I don’t agree with this 100%. But he has some valid points. Integrations tests are not a replacement for good unit tests. Use cucumber for happy paths. Use lower level tests for design and to isolate object behavior.

Cucumber is a good hammer

Cucumber is a good hammer

Not everything is a nail

I can skp teh unit testz?

Acceptance Tests

Application LevelFor CustomersSlowGood confidencePrevent against regression

Unit Tests

Object Level- Isolated!For developersFAST! (should be at least)- Tighter Feedback LoopMore about design!!!!!!!!!!!!

Will need both gears! Some things are easier to test at the application level and vice-versa.

SRSLY?Model specs,Controller Specs,and view specs!?

W

M

More tests == More Maintenance

Test Value =Design +

Documentation +Defence (regression)

if test.value > test.cost Suite.add(test)end

KTHXBYE!BenMabey.com

github.com/bmabey

Twitter: bmabeyIRC: mabes