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 Placing Text 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
Feature: title * not executed * documentation value * 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, tools are tools. While RSpec and Cucumber are optimized for BDD, using them doesn t automatically mean you re doing BDD" The RSpec Book
BDD is a mindset not a tool set
</rant>
Feature: title * not executed * documentation value * 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 Given /^a widget$/ do #codes go here
Step Definition Step Mother Given a widget Given /^a widget$/ do #codes go here
Step Definition Step Mother Given a widget Given /^a widget$/ do #codes go here
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 ping
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 rake cucumber:wip Limit tags in flow cucumber --tags @wip:2 --wip
@wip on master? $ rake -T cucumber rake cucumber:wip Limit tags in flow 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)
features/step_definitions/user_steps.rb Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true) Test Data Builder / Object Mother
features/step_definitions/user_steps.rb Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true) 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' )
features/step_definitions/user_steps.rb Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
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
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
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
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 features/support/env.rb require 'webrat' Webrat.configure do config config.mode = :rails
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 features/support/env.rb require 'webrat' Webrat.configure do config config.mode = :rails 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 features/step_definitions/webrat_steps.rb When /^I press "(.*)"$/ do button click_button(button) When /^I follow "(.*)"$/ do link click_link(link) 20+ Steps Out-of-box When /^I fill in "(.*)" with "(.*)"$/ do field, value fill_in(field, :with => value)
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
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
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
features/step_definitions/user_steps.rb Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true) visit new_session_path Specify fill_in "Email", outcome, :with not => @current_user.email implementation. 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")
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")
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 Then /^(.+) should appear on my wish list$/ do wish
features/step_definitions/wish_steps.rb When /^I make a "(.+)" wish$/ do wish Then /^(.+) should appear on my wish list$/ do wish 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 Then /^(.+) should appear on my wish list$/ do wish
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 Then /^(.+) should appear on my wish list$/ do wish response.should contain("your wish has been added!") response.should contain(wish)
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 No route matches /wishes with {:method=>:get} (ActionController::RoutingError) Then /^(.+) should appear on my wish list$/ do wish response.should contain("your wish has been added!") response.should contain(wish)
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
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 app/views/wishes/new.html.erb <% form_for :wish do f %> <%= f.label :name, "Wish" %> <%= f.text_field :name %> <%= submit_tag "Make the wish!" %> <% %>
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 app/views/wishes/new.html.erb <% form_for :wish do f %> <%= f.label :name, "Wish" %> <%= f.text_field :name %> <%= submit_tag "Make the wish!" %> <% %> Location Strategy FTW!
View Controller
spec/controllers/wishes_controller_spec.rb describe WishesController do describe "POST / (#create)" do
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'}
app/controllers/wishes_controller.rb class WishesController < ApplicationController def create current_user.wishes.create(params['wish'])
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)
app/controllers/wishes_controller.rb def create current_user.wishes.create(params['wish']) redirect_to :action => :index
View Controller Model
app/controllers/wishes_controller.rb def create current_user.wishes.create(params['wish']) redirect_to :action => :index 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 $./script generate rspec_model wish name:string user_id:integer
app/models/wish.rb class Wish < ActiveRecord::Base belongs_to :user app/models/user.rb class User < ActiveRecord::Base include Clearance::App::Models::User has_many :wishes
app/models/wish.rb class Wish < ActiveRecord::Base belongs_to :user app/models/user.rb When I make a New car wish Then New car should appear on my wish expected the following element s content to include Your wish has been added! class User < ActiveRecord::Base include Clearance::App::Models::User has_many :wishes
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!"
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!" app/controllers/wishes_controller.rb def create current_user.wishes.create(params['wish']) flash[:success] = "Your wish has been added!" redirect_to :action => :index
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!" app/controllers/wishes_controller.rb Then New car should appear on my wish expected the following element s content to include New car def create current_user.wishes.create(params['wish']) flash[:success] = "Your wish has been added!" redirect_to :action => :index
app/views/wishes/index.html.erb <ul> <% @wishes.each do wish %> <li><%= wish.name %></li> <% %> </ul>
spec/controllers/wishes_controller_spec.rb describe "GET / (#index)" do def do_get get :index it "assigns the user's wishes to the view" do do_get assigns[:wishes].should == @current_user.wishes
app/controllers/wishes_controller.rb def index @wishes = current_user.wishes
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 In order to save me time and headaches As a presenter of code I create a presentation in plaintext a'la Slidedown (from Pat Nakajima) and have CodeNote serve it up for me For example of how to test CLI tools take a look at CodeNote on github. RSpec and Cucumber also have good examples of how to do this. 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
Given the following presentation """ blah, blah """ Given /the following presentation$/ do presentation Yields the multi-line string
Given the following presentation """ blah, blah """ Given /the following presentation$/ do presentation CodeNote::PresentationLoader.setup(presentation)
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
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))
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))
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)) 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)) Helpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{cgi.escape(query)}" def canned_response_for(query)... return file_path
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)) def search_url_for(query) "http://search.twitter.com/search.json?q=#{cgi.escape(query)}" def canned_response_for(query)... return file_path
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)) def search_url_for(query) "http://search.twitter.com/search.json?q=#{cgi.escape(query)}" def canned_response_for(query)... return file_path
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)) Every time you monkeypatch def search_url_for(query) Object, a kitten dies. "http://search.twitter.com/search.json?q=#{cgi.escape(query)}" def canned_response_for(query)... return file_path
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)) module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{cgi.escape(query)}" def canned_response_for(query)... return file_path
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)) module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{cgi.escape(query)}" def canned_response_for(query)... return file_path 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)) module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{cgi.escape(query)}" def canned_response_for(query)... return file_path 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
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 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}")
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}") Then /I should see "(["]*)"$/ do text browser.should contain(text)
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
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 When the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see "And the winner is..." And I should see an ajax spinner @wip Scenario: winner is displayed Extract to Background
@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
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
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))
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.
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 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})
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.
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(' ','_') FakeTwitter.register_search(query, { :results => tweet_table.hashes})
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(' ','_') tweet_table.map_column!('created_at') do relative_time interpret_time(relative_time) FakeTwitter.register_search(query, { :results => tweet_table.hashes})
@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']) 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']) Spec::Matchers.define :contain do text, options match do browser options[:wait] = 0 browser.wait_until(options[:wait]) do browser.text.include?(text)
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']) Spec::Matchers.define :contain do text, options match do browser options[:wait] = 0 browser.wait_until(options[:wait]) do browser.text.include?(text) 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 Ninto Wii Candace CHEEZBURGER FuzzBuzz When I view the wish list for "Candace" Then I should see the following wishes Wish Ninto Wii
Given the following wishes exist Wish Family Member Laptop Thomas Ninto Wii Candace CHEEZBURGER FuzzBuzz features/step_definitions/wish_steps.rb Given /^the following wishes exist$/ do table
Given the following wishes exist Wish Family Member Laptop Thomas Ninto 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"])
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}"
Steps Within Steps When /^I view the wish list for "(.+)"$/ do user_name Given "I am logged in" visit "/wishes/#{user_name}"
Hooks Before do After do scenario World do World(MyModule) World(HerModule)
Tagged Hooks Before('@im_special', '@me_too') do @icecream = true @me_too Feature: Lorem Scenario: Ipsum Scenario: Dolor Feature: Sit @im_special Scenario: Amet 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 server 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 Application Level For Customers Slow Good confidence Prevent against regression Unit Tests Object Level- Isolated! For developers FAST! (should be at least) - Tighter Feedback Loop More about design!!!!!!!!!!!! Will need both gears! Some things are easier to test at the application level and vice-versa.
SRSLY? Model specs, Controller Specs, and view specs!?
W
M
More tests == More Maintenance
Test Value = Design + Documentation + Defence (regression)
if test.value > test.cost Suite.add(test)
KTHXBYE! BenMabey.com github.com/bmabey Twitter: bmabey IRC: mabes