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

Writing Software not Code with Cucumber

  • 1.
    Writing Software not code With Ben Mabey
  • 2.
    Writing Software not code With Ben Mabey
  • 3.
    Writing Software not code With Behaviour Driven Development Ben Mabey
  • 4.
  • 6.
    Tweet in the blanks... "most software projects are like _ _ _ _ _ _ _ _" #rubyhoedown #cucumber
  • 7.
    "most software projectsare like _ _ _ _ _ _ _ _" #rubyhoedown #cucumber
  • 10.
    So... why are softwareprojects like “The Homer”?
  • 14.
    Feature Devotion Text Placing emphasis on features instead of overall outcome
  • 16.
    Shingeo Shingo of Toyota says...
  • 17.
  • 18.
    "Inspection to find defectsis waste." "Inspection to prevent defects is essential."
  • 19.
    56% of allbugs are introduced in requirements. (CHAOS Report)
  • 20.
  • 21.
  • 23.
  • 25.
    * 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)
  • 26.
    There is notemplate. What is important to have in narrative: * business value * stakeholder role * user role * action to be taken by user
  • 27.
  • 28.
    Writing Software not code With Behaviour Driven Development Ben Mabey
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
    “All of thesetools 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
  • 34.
    BDD is a mindset not a tool set
  • 35.
  • 36.
    * 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)
  • 37.
    Scenario: title Given [Context] WhenI do [Action] Then I should see [Outcome]
  • 38.
    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]
  • 39.
  • 40.
    project_root/ | `-- features |-- awesomeness.feature |-- greatest_ever.feature
  • 41.
    project_root/ | `-- features |-- awesomeness.feature |-- greatest_ever.feature `-- support |-- env.rb `-- other_helpers.rb
  • 42.
    project_root/ | `-- features |-- awesomeness.feature |-- greatest_ever.feature `-- support |-- env.rb `-- other_helpers.rb |-- step_definitions | |-- domain_concept_A.rb | `-- domain_concept_B.rb
  • 43.
  • 44.
    Step Definition Given /^a widget$/ do Given a widget #codes go here end
  • 45.
    Step Definition Step Mother Given /^a widget$/ do Given a widget #codes go here end
  • 46.
    Step Definition Step Mother Given /^a widget$/ do Given a widget #codes go here end
  • 50.
  • 51.
  • 52.
    28+ Languages RSpec, Test::Unit, etc
  • 53.
    28+ Languages Your Code RSpec, Test::Unit, etc
  • 54.
  • 55.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
    REFACTOR and REPEAT
  • 69.
    features/manage_my_wishes.feature Feature: manage mywishes 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
  • 70.
    features/manage_my_wishes.feature Feature: manage mywishes 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
  • 71.
  • 72.
    Workflow git branch -badd_wish_tracker#
  • 73.
    Workflow git branch-b add_wish_tracker# Tag Scenario or Feature with @wip
  • 74.
    Workflow git branch-b add_wish_tracker# Tag Scenario or Feature with @wip cucumber --wip --tags @wip
  • 75.
    Workflow git branch-b add_wish_tracker# Tag Scenario or Feature with @wip cucumber --wip --tags @wip Develop it Outside-In
  • 76.
    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
  • 77.
    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!
  • 78.
  • 79.
    @wip on master? $rake -T cucumber
  • 80.
    @wip on master? $rake -T cucumber rake cucumber:ok OR rake cucumber
  • 81.
    @wip on master? $rake -T cucumber rake cucumber:ok OR rake cucumber cucumber --tags ~@wip --strict
  • 82.
    @wip on master? $rake -T cucumber Tag Exclusion rake cucumber:ok OR rake cucumber cucumber --tags ~@wip --strict
  • 83.
    @wip on master? $rake -T cucumber
  • 84.
    @wip on master? $rake -T cucumber rake cucumber:wip
  • 85.
    @wip on master? $rake -T cucumber rake cucumber:wip cucumber --tags @wip:2 --wip
  • 86.
    @wip on master? $rake -T cucumber in flow Limit tags rake cucumber:wip cucumber --tags @wip:2 --wip
  • 87.
    @wip on master? $rake -T cucumber in flow Limit tags rake cucumber:wip cucumber --tags @wip:2 --wip Expect failure - Success == Failure
  • 88.
    @wip on master? $rake -T cucumber rake cucumber:all Runs both ok and wip -- great for CI
  • 89.
    features/manage_my_wishes.feature Feature: manage mywishes 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
  • 91.
    Line # ofscenario
  • 93.
    Look Ma! backtraces! GivenI am logged in #features/manage_my_wishes.feature:8
  • 96.
    features/step_definitions/user_steps.rb Given /^I amlogged in$/ do @current_user = create_user(:email_confirmed => true) end
  • 97.
    features/step_definitions/user_steps.rb Given /^I amlogged in$/ do @current_user = create_user(:email_confirmed => true) end Test Data Builder / Object Mother
  • 98.
    features/step_definitions/user_steps.rb Given /^I amlogged 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
  • 99.
    features/step_definitions/user_steps.rb Given /^I amlogged in$/ do @current_user = create_user(:email_confirmed => true) end
  • 100.
    features/step_definitions/user_steps.rb Given /^I amlogged 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
  • 101.
    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
  • 102.
    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
  • 103.
    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
  • 104.
    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
  • 105.
    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
  • 106.
    features/step_definitions/user_steps.rb Given /^I amlogged 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
  • 107.
    features/step_definitions/user_steps.rb Given /^I amlogged 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
  • 108.
    features/step_definitions/user_steps.rb Given /^I amlogged 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
  • 109.
    features/step_definitions/user_steps.rb Given /^I amlogged 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
  • 110.
    features/step_definitions/user_steps.rb Given /^I amlogged 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
  • 112.
    No route matches“/sessions/create” with {:method=>:post} (ActionController::RoutingError)
  • 113.
  • 114.
    I’m going tocheat... $ gem install thoughtbot-clearance $ ./script generate clearance $ ./script generate clearance_features
  • 115.
  • 117.
    features/step_definitions/wish_steps.rb When /^I makea "(.+)" wish$/ do |wish| end Then /^(.+) should appear on my wish list$/ do |wish| end
  • 118.
    features/step_definitions/wish_steps.rb When /^I makea "(.+)" wish$/ do |wish| end Then /^(.+) should appear on my wish list$/ do |wish| end Regexp Capture -> Yielded Variable
  • 119.
    features/step_definitions/wish_steps.rb When /^I makea "(.+)" 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
  • 120.
    features/step_definitions/wish_steps.rb When /^I makea "(.+)" 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
  • 121.
    features/step_definitions/wish_steps.rb When /^I makea "(.+)" 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
  • 122.
  • 123.
    config/routes.rb ActionController::Routing::Routes.draw do |map| map.resources :wishes When I make a “New car” wish uninitialized constant WishesController (NameError)
  • 124.
    config/routes.rb ActionController::Routing::Routes.draw do|map| map.resources :wishes $./script generate rspec_controller new create
  • 125.
    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)
  • 126.
  • 127.
    app/views/wishes/index.html.erb <%= link_to "Makea wish", new_wish_path %> When I make a “New car” wish Could not find field: “Wish” (Webrat::NotFoundError)
  • 128.
    features/step_definitions/wish_steps.rb When /^I makea "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_button end
  • 129.
    features/step_definitions/wish_steps.rb When /^I makea "(.+)" 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 %>
  • 130.
    features/step_definitions/wish_steps.rb When /^I makea "(.+)" 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 %>
  • 131.
  • 132.
  • 133.
    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
  • 134.
    app/controllers/wishes_controller.rb class WishesController <ApplicationController def create current_user.wishes.create(params['wish']) end end
  • 135.
    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
  • 136.
    app/controllers/wishes_controller.rb def create current_user.wishes.create(params['wish']) redirect_to :action => :index end
  • 137.
  • 138.
    app/controllers/wishes_controller.rb defcreate 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)
  • 139.
    app/controllers/wishes_controller.rb defcreate current_user.wishes.create(params['wish']) redirect_to :action => :index end $./script generate rspec_model wish name:string user_id:integer
  • 140.
    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
  • 141.
    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
  • 142.
    spec/controllers/wishes_controller_spec.rb it "notifies theuser of creation via the flash" do do_post flash[:success].should == "Your wish has been added!" end
  • 143.
    spec/controllers/wishes_controller_spec.rb it "notifies theuser 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
  • 144.
    spec/controllers/wishes_controller_spec.rb it "should notifiesthe 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
  • 145.
    app/views/wishes/index.html.erb <ul> <% @wishes.each do|wish| %> <li><%= wish.name %></li> <% end %> </ul>
  • 146.
    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
  • 147.
    app/controllers/wishes_controller.rb def index @wishes = current_user.wishes end
  • 149.
    How do I testJS and AJAX? FAQ
  • 151.
  • 152.
    Slow Integrated Fast Isolated
  • 153.
    Slow Integrated Fast Isolated
  • 154.
    Slow Integrated Fast Isolated
  • 155.
    Slow Integrated Fast Isolated
  • 156.
    Slow Integrated Fast Isolated
  • 157.
  • 158.
    Slow Fast Joyful
  • 159.
    Slow Painful Fast Joyful
  • 160.
    Slow Painful Celerity Fast Joyful
  • 161.
  • 162.
    Celerity HtmlUnit
  • 163.
    Celerity HtmlUnit
  • 164.
    Celerity HtmlUnit
  • 165.
    Celerity HtmlUnit
  • 166.
    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'
  • 167.
    What if Iuse MRI?
  • 168.
  • 169.
    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'
  • 170.
    Celerity + http://github.com/dstrelau/webrat
  • 171.
    HtmlUnit + http://github.com/johnnyt/webrat
  • 172.
  • 173.
    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
  • 174.
    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
  • 175.
    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
  • 176.
    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
  • 177.
    @wip Scenario: waiting foran 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. """
  • 178.
    @wip Scenario: waiting foran 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. """
  • 179.
    @wip Scenario: waiting foran 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
  • 180.
    Given the followingpresentation """ blah, blah """ Given /the following presentation$/ do |presentation| end
  • 181.
    Given the followingpresentation """ blah, blah """ Given /the following presentation$/ do |presentation| end Yields the multi-line string
  • 182.
    Given the followingpresentation """ blah, blah """ Given /the following presentation$/ do |presentation| CodeNote::PresentationLoader.setup(presentation) end
  • 183.
  • 184.
    And no tweetshave been tweeted that match the '#free_stuff "aaron burr"' search
  • 185.
    How do I testweb services? FAQ
  • 186.
  • 187.
    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
  • 188.
    And no tweetshave been tweeted that match the '#free_stuff "aaron burr"' search Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| end
  • 189.
    And no tweetshave 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
  • 190.
    Given %r{no tweetshave been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query)) end
  • 191.
    Given %r{no tweetshave been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query)) end Helpers
  • 192.
    Given %r{no tweetshave 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
  • 193.
    Given %r{no tweetshave 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
  • 194.
    Given %r{no tweetshave 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
  • 195.
    Given %r{no tweetshave 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
  • 196.
    Given %r{no tweetshave 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
  • 197.
    Given %r{no tweetshave 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)
  • 198.
    Given %r{no tweetshave 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)
  • 199.
    When the presentergoes to the 3rd slide
  • 200.
    When the presentergoes 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
  • 201.
    When the presentergoes 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!
  • 202.
    And I goto the 3rd slide Then I should see "And the winner is..."
  • 203.
    And I goto 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
  • 204.
    And I goto 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
  • 205.
    And I shouldsee an ajax spinner
  • 206.
    And I shouldsee an ajax spinner Then /I should see an ajax spinner$/ do browser.image(:id, 'spinner').exists?.should be_true end
  • 207.
  • 208.
    Scenario: waiting foran 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
  • 209.
    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
  • 210.
    @wip Scenario: winner isdisplayed Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
  • 211.
    @wip Scenario: winner isdisplayed Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search Duplication of context!
  • 212.
    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
  • 213.
    @wip Scenario: winner isdisplayed 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
  • 214.
    When the followingtweets 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 |
  • 215.
    When the followingtweets 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
  • 216.
    When the followingtweets 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
  • 217.
    When the followingtweets 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
  • 218.
    When the followingtweets 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
  • 219.
    When the followingtweets 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...
  • 220.
    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">]
  • 221.
    When the followingtweets 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
  • 222.
    When the followingtweets 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
  • 223.
    When the followingtweets 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
  • 224.
    When the followingtweets 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
  • 225.
    @wip Scenario: winner isdisplayed 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
  • 226.
    Then %r{I shouldsee @([']+)'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
  • 227.
    Then %r{I shouldsee @([']+)'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
  • 228.
    Then %r{I shouldsee @([']+)'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
  • 229.
  • 230.
    Scenario: winner isdisplayed 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
  • 231.
  • 232.
  • 234.
    Scenario: view memberslist 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 |
  • 235.
    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
  • 236.
    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
  • 237.
  • 238.
    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 |
  • 239.
    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 |
  • 240.
    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 |
  • 241.
    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 |
  • 242.
    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 |
  • 243.
    Steps Within Steps When/^I view the wish list for "(.+)"$/ do |user_name| Given "I am logged in" visit "/wishes/#{user_name}" end
  • 244.
    Steps Within Steps When/^I view the wish list for "(.+)"$/ do |user_name| Given "I am logged in" visit "/wishes/#{user_name}" end
  • 245.
    Hooks Before do end After do|scenario| end World do end World(MyModule) World(HerModule)
  • 246.
    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
  • 247.
    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
  • 248.
  • 249.
    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.
  • 250.
  • 251.
    Cucumber is a goodhammer Not everything is a nail
  • 252.
    I can skpteh unit testz?
  • 253.
    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.
  • 254.
  • 258.
  • 259.
  • 260.
    More tests == MoreMaintenance
  • 261.
    Test Value = Design + Documentation + Defence (regression)
  • 262.
    if test.value >test.cost Suite.add(test) end
  • 263.
    KTHXBYE! BenMabey.com github.com/bmabey Twitter: bmabey IRC: mabes