• Share
  • Email
  • Embed
  • Like
  • Save
  • Private Content
Writing Software not Code with Cucumber
 

Writing Software not Code with Cucumber

on

  • 36,446 views

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.

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.

Statistics

Views

Total Views
36,446
Views on SlideShare
30,692
Embed Views
5,754

Actions

Likes
68
Downloads
514
Comments
1

41 Embeds 5,754

http://prodtest.sophiaapp.com 1128
http://prodstaging.sophiaapp.com 885
http://benmabey.com 658
http://www.benmabey.com 610
http://www.prodtest.sophiaapp.com 547
http://lean.sophiaapp.com 424
http://localhost:3000 384
http://www.prodstaging.sophiaapp.com 325
https://www.prodtest.sophiaapp.com 142
http://www.lean.sophiaapp.com 98
https://www.prodstaging.sophiaapp.com 95
http://wilearn.heroku.com 85
http://production.sophiaapp.com 45
http://devry-prodtest.sophiaapp.com 42
http://testschool.sophiaapp.com 39
http://natebean.com 35
http://devry-prodstaging.sophiaapp.com 26
http://demo.sophiaapp.com 24
http://localhost 23
http://www.slideshare.net 20
http://bundlr.com 20
http://qa.sophiaapp.com 18
https://www.lean.sophiaapp.com 14
http://micdstest.sophiaapp.com 12
http://stagingschool.sophiaapp.com 12
https://twitter.com 9
http://gobundlr.com 6
http://preproduction.sophiaapp.com 6
http://theoldreader.com 4
http://www.demo-sophia.org 3
http://feeds.feedburner.com 3
http://demotest.sophiaapp.com 2
http://webcache.googleusercontent.com 2
http://feeds2.feedburner.com 1
http://vega.deioc.ull.es 1
http://railsonedge.com 1
http://www.mefeedia.com 1
http://dev.gobundlr.com 1
http://static.slidesharecdn.com 1
http://www.newsblur.com 1
http://www.mybestcv2.co.il 1
More...

Accessibility

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel

11 of 1 previous next

  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

    Writing Software not Code with Cucumber Writing Software not Code with Cucumber Presentation Transcript

    • Writing Software not code With Ben Mabey
    • Writing Software not code With Ben Mabey
    • Writing Software not code With Behaviour Driven Development Ben Mabey
    • ?
    • 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 Text Placing emphasis on features instead of overall outcome
    • Shingeo Shingo of Toyota says...
    • "Inspection to find defects is waste."
    • "Inspection to find defects is waste." "Inspection to prevent defects is essential."
    • 56% of all bugs are introduced in requirements. (CHAOS Report)
    • Root Cause Analysis
    • Popping the Why Stack...
    • Protect Revenue Increase Revenue Manage Cost
    • * not executed * documentation value Feature: title * variant of contextra * business value up front In order to [Business Value] As a [Role] I want to [Some Action] (feature)
    • There is no template. What is important to have in narrative: * business value * stakeholder role * user role * action to be taken by user
    • <rant>
    • Writing Software not code With Behaviour Driven Development Ben Mabey
    • != 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>
    • * not executed * documentation value Feature: title * variant of contextra * business value up front In order to [Business Value] As a [Role] I want to [Some Action] (feature)
    • Scenario: title Given [Context] When I do [Action] Then I should see [Outcome]
    • Scenario: title Given [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 Definition Given /^a widget$/ do Given a widget #codes go here end
    • Step Definition Step Mother Given /^a widget$/ do Given a widget #codes go here end
    • Step Definition Step Mother Given /^a widget$/ do Given a widget #codes go here end
    • 28+ Languages
    • 28+ Languages
    • 28+ Languages RSpec, Test::Unit, etc
    • 28+ Languages Your Code RSpec, Test::Unit, etc
    • 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...
    • REFACTOR and 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 Work In Progress 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 @proposed Scenario: remove wish @proposed Scenario: tweet wish
    • 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 @wip Develop it Outside-In
    • Workflow git branch -b add_wish_tracker# Tag Scenario or Feature with @wip cucumber --wip --tags @wip Develop it Outside-In git rebase ---interactive; git merge
    • Workflow git branch -b add_wish_tracker# Tag Scenario or Feature with @wip cucumber --wip --tags @wip Develop it Outside-In git rebase ---interactive; git merge Repeat!
    • @wip on master?
    • @wip on master? $ rake -T cucumber
    • @wip on master? $ rake -T cucumber rake cucumber:ok OR rake cucumber
    • @wip on master? $ rake -T cucumber rake cucumber:ok OR rake cucumber cucumber --tags ~@wip --strict
    • @wip on master? $ rake -T cucumber Tag Exclusion rake cucumber:ok OR rake cucumber cucumber --tags ~@wip --strict
    • @wip on master? $ rake -T cucumber
    • @wip on master? $ rake -T cucumber rake cucumber:wip
    • @wip on master? $ rake -T cucumber rake cucumber:wip cucumber --tags @wip:2 --wip
    • @wip on master? $ rake -T cucumber in flow Limit tags rake cucumber:wip cucumber --tags @wip:2 --wip
    • @wip on master? $ rake -T cucumber in flow Limit tags rake cucumber:wip cucumber --tags @wip:2 --wip Expect failure - Success == Failure
    • @wip on master? $ rake -T cucumber rake 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 @proposed Scenario: remove wish @proposed Scenario: 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 Fixture Replacement, Fixjour, Factory Girl, etc 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' ) end end
    • 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_button end
    • features/step_definitions/user_steps.rb Webrat / Awesomeness 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 Webrat / Awesomeness 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 Webrat / Awesomeness 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/support/env.rb require 'webrat' Webrat.configure do |config| config.mode = :rails end
    • features/step_definitions/user_steps.rb Webrat / Awesomeness 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/support/env.rb require 'webrat' Webrat.configure do |config| config.mode = :rails end Adapter
    • features/step_definitions/user_steps.rb Webrat / Awesomeness 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/webrat_steps.rb When /^I press "(.*)"$/ do |button| click_button(button) end 20+ Steps Out-of-box When /^I follow "(.*)"$/ do |link| click_link(link) end When /^I fill in "(.*)" with "(.*)"$/ do |field, value| fill_in(field, :with => value) 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_button 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_button # make sure we have actually logged in- so we fail fast if not session[:user_id].should == @current_user.id 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_button # 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 end
    • features/step_definitions/user_steps.rb Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true) visit new_session_path Specify outcome, not implementation. 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 session[:user_id].should == @current_user.id controller.current_user.should == @current_user response.should contain("Signed in successfully") 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_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
    • Authlogic? http://github.com/hectoregm/groundwork
    • 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_button end Then /^(.+) should appear on my wish list$/ do |wish| end
    • 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_button end Then /^(.+) should appear on my wish list$/ do |wish| response.should contain("Your wish has been added!") response.should contain(wish) end
    • 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_button end No route matches “/wishes” with {:method=>:get} appear on my wish list$/ do |wish| Then /^(.+) should (ActionController::RoutingError) response.should contain("Your wish has been added!") response.should contain(wish) end
    • 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” wish uninitialized 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” wish Could not find link with text or title or id “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_button end
    • 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_button end 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 %>
    • 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_button end Location Strategy FTW! 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 %>
    • View Controller
    • spec/controllers/wishes_controller_spec.rb describe WishesController do describe "POST / (#create)" do end end
    • 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 end end
    • 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 ..... it "redirects the user to their wish list" do do_post response.should redirect_to(wishes_path) end end 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” wish undefined 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 :user end app/models/user.rb class User < ActiveRecord::Base include Clearance::App::Models::User has_many :wishes end
    • app/models/wish.rb class Wish < ActiveRecord::Base belongs_to :user end app/models/user.rb When I make a “New car” wish Then “New <car” should appear on my wish class User ActiveRecord::Base include Clearance::App::Models::User has_many the following element’s content to include expected:wishes “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
    • 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 app/controllers/wishes_controller.rb def create current_user.wishes.create(params['wish']) flash[:success] = "Your wish has been added!" redirect_to :action => :index 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 app/controllers/wishes_controller.rb Then “New car” should appear on my wish expected the following element’s content to include def create “New car” current_user.wishes.create(params['wish']) flash[:success] = "Your wish has been added!" redirect_to :action => :index end
    • 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
    • How do I test JS and AJAX? FAQ
    • Slow Fast
    • Slow Integrated Fast Isolated
    • Slow Integrated Fast Isolated
    • Slow Integrated Fast Isolated
    • Slow Integrated Fast Isolated
    • Slow Integrated Fast Isolated
    • Slow Fast
    • Slow Fast Joyful
    • Slow Painful Fast Joyful
    • Slow Painful Celerity Fast Joyful
    • Celerity
    • Celerity HtmlUnit
    • Celerity HtmlUnit
    • Celerity HtmlUnit
    • Celerity HtmlUnit
    • 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?
    • Culerity http://github.com/langalex/culerity
    • 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'
    • Celerity + http://github.com/dstrelau/webrat
    • HtmlUnit + http://github.com/johnnyt/webrat
    • CodeNote http://github.com/bmabey/codenote
    • Feature: CLI Server For example of how to In order to save me time and headaches test CLI tools take a look As a presenter of code at CodeNote on github. I create a presentation in plaintext RSpec and Cucumber also have good examples of a'la Slidedown (from Pat Nakajima) how to do this. 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
    • 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
    • @wip Scenario: 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. """
    • @wip Scenario: 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. """
    • @wip 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
    • Given the following presentation """ blah, blah """ Given /the following presentation$/ do |presentation| end
    • Given the following presentation """ blah, blah """ Given /the following presentation$/ do |presentation| end Yields the multi-line string
    • Given the following presentation """ blah, blah """ Given /the following presentation$/ do |presentation| CodeNote::PresentationLoader.setup(presentation) end
    • RSpec Cycle
    • And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
    • How do I test web services? FAQ
    • http://github.com/chrisk/fakeweb
    • 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
    • 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| 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
    • 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 Helpers 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
    • 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 “Every time you monkeypatch def search_url_for(query) Object, a kitten dies.” "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 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 end end
    • 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 end end 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_true end
    • 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
    • @wip Scenario: winner is displayed Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
    • @wip Scenario: 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 Extract to ‘Background’ 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
    • @wip 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
    • 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 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| Cucumber::AST::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| 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)) Umm... that won’t work. 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)) What I would really like is a test data builder/factory for end twitter searches...
    • http://github.com/bmabey/faketwitter require '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 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 | When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeTwitter.register_search(query, { :results => tweet_table.hashes}) Our headers and columns aren’t compatible with API. 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 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
    • @wip 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
    • 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 end end
    • 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 end end Keep trying after sleeping until 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| end end
    • 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| 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"]) end end
    • Table Diffing http://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 |
    • 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 |
    • 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 |
    • 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 |
    • Steps Within Steps 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| Given "I am logged in" visit "/wishes/#{user_name}" end
    • Hooks Before do end After do |scenario| end World do end World(MyModule) World(HerModule)
    • Tagged Hooks Before('@im_special', '@me_too') do @icecream = true end @me_too Feature: Sit Feature: Lorem @im_special Scenario: Ipsum Scenario: Amet Scenario: Dolor Scenario: Consec
    • 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 ser ver that forks.. hence Spork. :) http://github.com/timcharper/spork
    • Drinking the Cucumber Kool-Aid?
    • Integration tests are a scam J. B. Rainsberger http://www.jbrains.ca/permalink/239 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 Unit Tests Application Level Object Level- Isolated! For Customers For developers Slow FAST! (should be at least) Good confidence - Tighter Feedback Loop Prevent against More about design!!!!!!!!!!!! regression 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: bmabey IRC: mabes