writing software not code with cucumber
DESCRIPTION
Describes Outside-In development and Behvaiour Driven Development. Illustrates basic Cucumber usage within a Rails app and then goes over more advanced topics such as JS as web services.TRANSCRIPT
With
Ben Mabey
Writing Software not code
With
Ben Mabey
Writing Software not code
With
Ben Mabey
Writing Software not code
Behaviour Driven Development
?
Tweet in the blanks...
"most software projects are like _ _ _ _ _ _ _ _"
#rubyhoedown #cucumber
"most software projects are like _ _ _ _ _ _ _ _"
#rubyhoedown #cucumber
So... why are software projects like “The Homer”?
Feature Devotion
TextPlacing emphasis on features instead of
overall outcome
Shingeo Shingo of Toyota says...
"Inspection to find defects is waste."
"Inspection to prevent defects is
essential."
"Inspection to find defects is waste."
56% of all bugs are introduced in requirements. (CHAOS Report)
Root Cause Analysis
Popping the Why Stack...
Protect Revenue
Increase Revenue
Manage Cost
Feature: title
In order to [Business Value]As a [Role]I want to [Some Action] (feature)
* not executed* documentation value* variant of contextra* business value up front
There is no template.What is important to have in narrative:
* business value * stakeholder role * user role * action to be taken by user
<rant>
With
Ben Mabey
Writing Software not code
Behaviour Driven Development
!= BDD
!= BDD
RSpec != BDD
RSpec != BDD
“All of these tools are great... but, in the end, tools are tools. While RSpec and Cucumber are optimized for BDD, using them
doesn’t automatically mean you’re doing BDD"
The RSpec Book
BDD is a mindset
not a tool set
</rant>
Feature: title
In order to [Business Value]As a [Role]I want to [Some Action] (feature)
* not executed* documentation value* variant of contextra* business value up front
Scenario: titleGiven [Context]When I do [Action]Then I should see [Outcome]
Scenario: titleGiven [Context]And [More Context]When I do [Action]And [Other Action]Then I should see [Outcome]But I should not see [Outcome]
project_root/| `-- features
project_root/| `-- features |-- awesomeness.feature |-- greatest_ever.feature
project_root/| `-- features |-- awesomeness.feature |-- greatest_ever.feature `-- support |-- env.rb `-- other_helpers.rb
project_root/| `-- features |-- awesomeness.feature |-- greatest_ever.feature `-- support |-- env.rb `-- other_helpers.rb |-- step_definitions | |-- domain_concept_A.rb | `-- domain_concept_B.rb
Step
Given a widget
Step
Given a widgetGiven /^a widget$/ do #codes go hereend
Definition
Step
Given a widgetGiven /^a widget$/ do #codes go hereend
Definition
Step Mother
Step
Given a widgetGiven /^a widget$/ do #codes go hereend
Definition
Step Mother
a.featureb.feature
a.featureb.feature
a.featureb.feature
a.featureb.feature 28+
Languages
a.featureb.feature
x_steps.rby_steps.rb
28+ Languages
a.featureb.feature
x_steps.rby_steps.rb
RSpec, Test::Unit, etc
28+ Languages
a.featureb.feature
x_steps.rby_steps.rb
Your Code
RSpec, Test::Unit, etc
28+ Languages
Not Just for Rails
Outside-In
Write Scenarios
Steps are pending
Write Step Definition
Go Down A Gear
RSpec, TestUnit, etc
Write Code Example(Unit Test)
Make Example Pass
REFACTOR!!
Where Are we?
Continue until...
REFACTORand
REPEAT
features/manage_my_wishes.feature
Feature: manage my wishes
In order to get more stuff As a greedy person I want to manage my wish list for my family members to view @proposed Scenario: add wish
@proposed Scenario: remove wish
@proposed Scenario: tweet wish
features/manage_my_wishes.feature
Feature: manage my wishes
In order to get more stuff As a greedy person I want to manage my wish list for my family members to view
@wip Scenario: add wish Given I am logged in When I make a "New car" wish Then "New car" should appear on my wish list
@proposedScenario: remove wish
@proposedScenario: tweet wish
Work In Progress
Workflow
Workflow
git branch -b add_wish_tracker#
Workflow
git branch -b add_wish_tracker#Tag Scenario or Feature with @wip
Workflow
git branch -b add_wish_tracker#Tag Scenario or Feature with @wip
cucumber --wip --tags @wip
Workflow
git branch -b add_wish_tracker#Tag Scenario or Feature with @wip
cucumber --wip --tags @wipDevelop it Outside-In
Workflow
git branch -b add_wish_tracker#Tag Scenario or Feature with @wip
cucumber --wip --tags @wipDevelop it Outside-In
git rebase ---interactive; git merge
Workflow
git branch -b add_wish_tracker#Tag Scenario or Feature with @wip
cucumber --wip --tags @wipDevelop it Outside-In
git rebase ---interactive; git mergeRepeat!
@wip on master?
$ rake -T cucumber
@wip on master?
$ rake -T cucumberrake cucumber:ok OR rake cucumber
@wip on master?
$ rake -T cucumberrake cucumber:ok OR rake cucumber
cucumber --tags ~@wip --strict
@wip on master?
$ rake -T cucumberrake cucumber:ok OR rake cucumber
cucumber --tags ~@wip --strict
@wip on master?Tag Exclusion
@wip on master?$ rake -T cucumber
@wip on master?$ rake -T cucumberrake cucumber:wip
cucumber --tags @wip:2 --wip
@wip on master?$ rake -T cucumberrake cucumber:wip
cucumber --tags @wip:2 --wip
@wip on master?$ rake -T cucumberrake cucumber:wip
Limit tags in flow
cucumber --tags @wip:2 --wip
@wip on master?$ rake -T cucumberrake cucumber:wip
Limit tags in flow
Expect failure - Success == Failure
@wip on master?$ rake -T cucumberrake cucumber:all
Runs both ok and wip -- great for CI
features/manage_my_wishes.feature
Feature: manage my wishes
In order to get more stuff As a greedy person I want to manage my wish list for my family members to view
@wip Scenario: add wish Given I am logged in When I make a "New car" wish Then "New car" should appear on my wish list
@proposedScenario: remove wish
@proposedScenario: tweet wish
Line # of scenario
Look Ma! backtraces!Given I am logged in #features/manage_my_wishes.feature:8
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
end
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
end
Test Data Builder / Object Mother
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
end
spec/fixjour_builders.rb
Fixjour do define_builder(User) do |klass, overrides| klass.new( :email => "user#{counter(:user)}@email.com", :password => 'password', :password_confirmation => 'password' ) endend
Fixture Replacement, Fixjour, Factory Girl, etc
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
end
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend
Webrat / Awesomeness
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend
Webrat / Awesomeness
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend
features/support/env.rb
require 'webrat'
Webrat.configure do |config| config.mode = :railsend
Webrat / Awesomeness
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend
features/support/env.rb
require 'webrat'
Webrat.configure do |config| config.mode = :railsend
Adapter
Webrat / Awesomeness
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend
features/step_definitions/webrat_steps.rb
When /^I press "(.*)"$/ do |button| click_button(button)end
When /^I follow "(.*)"$/ do |link| click_link(link)end
When /^I fill in "(.*)" with "(.*)"$/ do |field, value| fill_in(field, :with => value) end
When /^I select "(.*)" from "(.*)"$/ do |value, field| select(value, :from => field) end
# Use this step in conjunction with Rail's datetime_select helper. For example:# When I select "December 25, 2008 10:00" as the date and time When /^I select "(.*)" as the date and time$/ do |time| select_datetime(time)end
# Use this step when using multiple datetime_select helpers on a page or # you want to specify which datetime to select. Given the following view:# <%= f.label :preferred %><br /># <%= f.datetime_select :preferred %># <%= f.label :alternative %><br /># <%= f.datetime_select :alternative %># The following steps would fill out the form:# When I select "November 23, 2004 11:20" as the "Preferred" data and time# And I select "November 25, 2004 10:30" as the "Alternative" data and timeWhen /^I select "(.*)" as the "(.*)" date and time$/ do |datetime, datetime_label| select_datetime(datetime, :from => datetime_label)end
# Use this step in conjunction with Rail's time_select helper. For example:# When I select "2:20PM" as the time# Note: Rail's default time helper provides 24-hour time-- not 12 hour time. Webrat# will convert the 2:20PM to 14:20 and then select it. When /^I select "(.*)" as the time$/ do |time| select_time(time)end
# Use this step when using multiple time_select helpers on a page or you want to# specify the name of the time on the form. For example:# When I select "7:30AM" as the "Gym" timeWhen /^I select "(.*)" as the "(.*)" time$/ do |time, time_label| select_time(time, :from => time_label)end
# Use this step in conjunction with Rail's date_select helper. For example:# When I select "February 20, 1981" as the dateWhen /^I select "(.*)" as the date$/ do |date| select_date(date)end
# Use this step when using multiple date_select helpers on one page or# you want to specify the name of the date on the form. For example:# When I select "April 26, 1982" as the "Date of Birth" dateWhen /^I select "(.*)" as the "(.*)" date$/ do |date, date_label| select_date(date, :from => date_label)end
When /^I check "(.*)"$/ do |field| check(field) end
When /^I uncheck "(.*)"$/ do |field| uncheck(field) end
When /^I choose "(.*)"$/ do |field| choose(field)end
When /^I attach the file at "(.*)" to "(.*)" $/ do |path, field| attach_file(field, path)end
Then /^I should see "(.*)"$/ do |text| response.should contain(text)end
Then /^I should not see "(.*)"$/ do |text| response.should_not contain(text)end
Then /^the "(.*)" checkbox should be checked$/ do |label| field_labeled(label).should be_checkedend
Webrat / Awesomeness
20+ Steps Out-of-box
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_buttonend
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_button
end
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do
# make sure we have actually logged in- so we fail fast if not session[:user_id].should == @current_user.idend
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do
# make sure we have actually logged in- so we fail fast if not controller.current_user.should == @current_userend
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_button session[:user_id].should == @current_user.id
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_button
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do
# make sure we have actually logged in- so we fail fast if not session[:user_id].should == @current_user.id controller.current_user.should == @current_user response.should contain("Signed in successfully")end
Specify outcome, not implementation.
features/step_definitions/user_steps.rb
Given /^I am logged in$/ do @current_user = create_user(:email_confirmed => true)
visit new_session_path fill_in "Email", :with => @current_user.email fill_in "Password", :with => valid_user_attributes["password"] click_button # make sure we have actually logged in- so we fail fast if not response.should contain("Signed in successfully")end
No route matches “/sessions/create” with{:method=>:post} (ActionController::RoutingError)
I’m going to cheat...
I’m going to cheat...$ gem install thoughtbot-clearance$ ./script generate clearance$ ./script generate clearance_features
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_buttonend
Then /^(.+) should appear on my wish list$/ do |wish| end
features/step_definitions/wish_steps.rb
Then /^(.+) should appear on my wish list$/ do |wish| response.should contain("Your wish has been added!") response.should contain(wish)end
When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend
features/step_definitions/wish_steps.rb
When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend
Then /^(.+) should appear on my wish list$/ do |wish| response.should contain("Your wish has been added!") response.should contain(wish)end
No route matches “/wishes” with{:method=>:get} (ActionController::RoutingError)
config/routes.rb
ActionController::Routing::Routes.draw do |map| map.resources :wishes
config/routes.rb
ActionController::Routing::Routes.draw do |map| map.resources :wishes
When I make a “New car” wishuninitialized constant WishesController (NameError)
config/routes.rb
ActionController::Routing::Routes.draw do |map| map.resources :wishes
$./script generate rspec_controller new create
config/routes.rb
ActionController::Routing::Routes.draw do |map| map.resources :wishes
When I make a “New car” wishCould not find link with text or title orid “Make a wish” (Webrat::NotFoundError)
app/views/wishes/index.html.erb
<%= link_to "Make a wish", new_wish_path %>
app/views/wishes/index.html.erb
<%= link_to "Make a wish", new_wish_path %>
When I make a “New car” wish Could not find field: “Wish” (Webrat::NotFoundError)
features/step_definitions/wish_steps.rb
When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend
features/step_definitions/wish_steps.rb
When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend
app/views/wishes/new.html.erb
<% form_for :wish do |f| %> <%= f.label :name, "Wish" %> <%= f.text_field :name %> <%= submit_tag "Make the wish!" %><% end %>
fill_in "Wish", :with => wish
<%= f.label :name, "Wish" %> <%= f.text_field :name %>
features/step_definitions/wish_steps.rb
When /^I make a "(.+)" wish$/ do |wish| visit "/wishes" click_link "Make a wish" fill_in "Wish", :with => wish click_buttonend
app/views/wishes/new.html.erb
<% form_for :wish do |f| %> <%= submit_tag "Make the wish!" %><% end %>
Location Strategy FTW!
View
Controller
spec/controllers/wishes_controller_spec.rb
describe WishesController do describe "POST / (#create)" do
endend
spec/controllers/wishes_controller_spec.rb
describe WishesController do describe "POST / (#create)" do it "creates a new wish for the user with the params" do user = mock_model(User, :wishes => mock("wishes association")) controller.stub!(:current_user).and_return(user) user.wishes.should_receive(:create).with(wish_params)
post :create, 'wish' => {'name' => 'Dog'} end endend
app/controllers/wishes_controller.rb
class WishesController < ApplicationController
def create current_user.wishes.create(params['wish']) end
end
spec/controllers/wishes_controller_spec.rb
describe WishesController do describe "POST / (#create)" do before(:each) do .....
endend
spec/controllers/wishes_controller_spec.rb
it "redirects the user to their wish list" do do_post response.should redirect_to(wishes_path) end
app/controllers/wishes_controller.rb
def create current_user.wishes.create(params['wish']) redirect_to :action => :index end
View
Controller
Model
app/controllers/wishes_controller.rb
def create current_user.wishes.create(params['wish']) redirect_to :action => :index end
When I make a “New car” wishundefined method `wishes` for #<User:0x268e898> (NoMethodError)
app/controllers/wishes_controller.rb
def create current_user.wishes.create(params['wish']) redirect_to :action => :index end
$./script generate rspec_model wish name:string user_id:integer
app/models/wish.rb
class Wish < ActiveRecord::Base belongs_to :userend
app/models/user.rb
class User < ActiveRecord::Base include Clearance::App::Models::User has_many :wishesend
app/models/wish.rb
class Wish < ActiveRecord::Base belongs_to :userend
app/models/user.rb
class User < ActiveRecord::Base include Clearance::App::Models::User has_many :wishesend
When I make a “New car” wishThen “New car” should appear on my wishexpected the following element’s content to include “Your wish has been added!”
spec/controllers/wishes_controller_spec.rb
it "notifies the user of creation via the flash" do do_post flash[:success].should == "Your wish has been added!"end
current_user.wishes.create(params['wish']) redirect_to :action => :index
app/controllers/wishes_controller.rb
def create flash[:success] = "Your wish has been added!" end
spec/controllers/wishes_controller_spec.rb
it "notifies the user of creation via the flash" do do_post flash[:success].should == "Your wish has been added!"end
current_user.wishes.create(params['wish']) redirect_to :action => :index
app/controllers/wishes_controller.rb
def create flash[:success] = "Your wish has been added!" end
spec/controllers/wishes_controller_spec.rb
it "should notifies the user of creation via the flash" do do_post flash[:success].should == "Your wish has been added!"end
Then “New car” should appear on my wishexpected the following element’s content to include “New car”
app/views/wishes/index.html.erb
<ul><% @wishes.each do |wish| %> <li><%= wish.name %></li><% end %></ul>
spec/controllers/wishes_controller_spec.rb
describe "GET / (#index)" do def do_get get :index end
it "assigns the user's wishes to the view" do do_get assigns[:wishes].should == @current_user.wishes end
end
app/controllers/wishes_controller.rb
def index @wishes = current_user.wishes end
FAQ
How do I test JS and
AJAX?
Slow
Fast
Slow
Fast
Integrated
Isolated
Slow
Fast
Integrated
Isolated
Slow
Fast
Integrated
Isolated
Slow
Fast
Integrated
Isolated
Slow
Fast
Integrated
Isolated
Slow
Fast
Slow
Fast Joyful
Slow
Fast
Painful
Joyful
Slow
Fast
Painful
Joyful
Celerity
Celerity
CelerityHtmlUnit
CelerityHtmlUnit
CelerityHtmlUnit
CelerityHtmlUnit
require "rubygems"require "celerity"
browser = Celerity::Browser.new
browser.goto('http://www.google.com')browser.text_field(:name, 'q').value = 'Celerity'browser.button(:name, 'btnG').click
puts "yay" if browser.text.include? 'celerity.rubyforge.org'
What if I use MRI?
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
Scenario: basic presentation loading and viewing Given that the codenote server is not running And a file named "presentation.md" with: """ !TITLE My Presentation !PRESENTER Ben Mabey # This is the title slide !SLIDE # This is second slide... """ When I run "codenote_load presentation.md" And I run "codenote" And I visit the servers address
For example of how to test CLI tools take a look at CodeNote on github.
RSpec and Cucumber also have good examples of how to do this.
Feature: Twitter Quiz In order to encourage audience participation where 90% of the audience is hacking on laptops As a presenter I want audience members To answer certain questions via twitter
Feature: Twitter Quiz In order to encourage audience participation where 90% of the audience is hacking on laptops As a presenter I want audience members To answer certain questions via twitter
@proposed Scenario: waiting for an answer
@proposed Scenario: winner is displayed
@proposed Scenario: fail whale
@proposed Scenario: network timeout
Feature: Twitter Quiz In order to encourage audience participation where 90% of the audience is hacking on laptops As a presenter I want audience members To answer certain questions via twitter
@wip Scenario: waiting for an answer
@wipScenario: waiting for an answer Given the following presentation """ !TITLE American History !PRESENTER David McCullough # Wanna win a prize? ### You'll have to answer a question... ### in a tweet! First correct tweet wins! !SLIDE # Who shot Alexander Hamilton? ## You must use #free_stuff in your tweet. !DYNAMIC-SLIDE TwitterQuiz '#free_stuff "aaron burr"' !SLIDE Okay, that was fun. Lets actually start now. """
!DYNAMIC-SLIDE TwitterQuiz '#free_stuff "aaron burr"'
@wipScenario: waiting for an answer Given the following presentation """ !TITLE American History !PRESENTER David McCullough # Wanna win a prize? ### You'll have to answer a question... ### in a tweet! First correct tweet wins! !SLIDE # Who shot Alexander Hamilton? ## You must use #free_stuff in your tweet. !SLIDE Okay, that was fun. Lets actually start now. """
@wipScenario: waiting for an answer Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search When the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see "And the winner is..." And I should see an ajax spinner
Given /the following presentation$/ do |presentation| end
Given the following presentation """ blah, blah """
Given /the following presentation$/ do |presentation| end
Given the following presentation """ blah, blah """
Yields the multi-line string
CodeNote::PresentationLoader.setup(presentation)
Given /the following presentation$/ do |presentation| end
Given the following presentation """ blah, blah """
RSpec Cycle
And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
FAQ
How do I test webservices?
http://github.com/chrisk/fakeweb
http://github.com/chrisk/fakeweb
page = `curl -is http://www.google.com/`FakeWeb.register_uri(:get, "http://www.google.com/",
:response => page)
Net::HTTP.get(URI.parse("http://www.google.com/")) # => Full response, including headers
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| end
And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
Helpers
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end
def canned_response_for(query) .... return file_path end
Helpers
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end
def canned_response_for(query) .... return file_path end
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end
def canned_response_for(query) .... return file_path end
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end
def canned_response_for(query) .... return file_path end
“Every time you monkeypatch Object, a kitten dies.”
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end
def canned_response_for(query) .... return file_path end
end
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end
def canned_response_for(query) .... return file_path end
end
World(TwitterHelpers)
Given %r{no tweets have been tweeted that match the '([']*)' search$} do |query| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))end
module TwitterHelpers def search_url_for(query) "http://search.twitter.com/search.json?q=#{CGI.escape(query)}" end
def canned_response_for(query) .... return file_path end
end
World(TwitterHelpers)
When the presenter goes to the 3rd slide
When the presenter goes to the 3rd slide
When /the presenter goes to the (\d+)(?:st|nd|rd|th) slide$/ do |slide_number|
presenter_browser.goto path('/') (slide_number.to_i - 1).times do presenter_browser.link(:text, "Next").click endend
When the presenter goes to the 3rd slide
When /the presenter goes to the (\d+)(?:st|nd|rd|th) slide$/ do |slide_number|
presenter_browser.goto path('/') (slide_number.to_i - 1).times do presenter_browser.link(:text, "Next").click endend
Presenter has own browser,multiple sessions!
And I go to the 3rd slide Then I should see "And the winner is..."
And I go to the 3rd slide Then I should see "And the winner is..."
When /I go to the (\d+)(?:st|nd|rd|th) slide$/ do |slide_number| browser.goto path("/slides/#{slide_number}")end
And I go to the 3rd slide Then I should see "And the winner is..."
When /I go to the (\d+)(?:st|nd|rd|th) slide$/ do |slide_number| browser.goto path("/slides/#{slide_number}")end
Then /I should see "(["]*)"$/ do |text| browser.should contain(text)end
And I should see an ajax spinner
And I should see an ajax spinner
Then /I should see an ajax spinner$/ do browser.image(:id, 'spinner').exists?.should be_trueend
Brief RSpec cycle?
Scenario: waiting for an answer Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search When the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see "And the winner is..." And I should see an ajax spinner
Feature: Twitter Quiz In order to encourage audience participation where 90% of the audience is hacking on laptops As a presenter I want audience members To answer certain questions via twitter
Scenario: waiting for an answer ..... @wip Scenario: winner is displayed
@wipScenario: winner is displayed Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
@wipScenario: winner is displayed Given the following presentation ... And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
Duplication of context!
Feature: Twitter Quiz ... Background: A presentation with a Twitter Quiz
Given the following presentation """ blah, blah """ And no tweets have been tweeted that match the '#free_stuff "aaron burr"' search
Scenario: waiting for an answer When the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see "And the winner is..." And I should see an ajax spinner
@wip Scenario: winner is displayed
Extract to ‘Background’
@wipScenario: winner is displayed When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search | From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago | And the presenter goes to the 3rd slide And I go to the 3rd slide
Then I should see @jefferson's tweet along with his avatar
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table|
end
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table|
end
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
Cucumber::AST::Table
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))
end
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))
end
Umm... that won’t work.
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeWeb.register_uri(:get, search_url_for(query), :body => canned_response_for(query))
end
What I would really like is atest data builder/factory for
twitter searches...
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
http://github.com/bmabey/faketwitterrequire 'faketwitter'
FakeTwitter.register_search("#cheese", {:results => [{:text => "#cheese is good"}]})
require 'twitter_search'TwitterSearch::Client.new('').query('#cheese')=> [#<TwitterSearch::Tweet:0x196cef8 @id=1, @text="#cheese is good", @created_at="Fri, 21 Aug 2009 09:31:27 +0000", @to_user_id=nil, @from_user_id=1, @to_user=nil, @source="<a href="http://twitter.com/">web</a>", @iso_language_code="en", @from_user="jojo", @language="en", @profile_image_url="http://s3.amazonaws.com/twitter_production/profile_images/1/photo.jpg">]
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeTwitter.register_search(query, { :results => tweet_table.hashes})
end
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| FakeTwitter.register_search(query, { :results => tweet_table.hashes})
end
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
Our headers and columnsaren’t compatible with API.
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| tweet_table.map_headers! do |header| header.downcase.gsub(' ','_') end
FakeTwitter.register_search(query, {:results => tweet_table.hashes})
end
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
When %r{the following tweets are tweeted that match the '([']*)' search$} do |query, tweet_table| tweet_table.map_headers! do |header| header.downcase.gsub(' ','_') end
tweet_table.map_column!('created_at') do |relative_time| interpret_time(relative_time) end
FakeTwitter.register_search(query, {:results => tweet_table.hashes})
end
When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search
| From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago |
@wipScenario: winner is displayed When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search | From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago | And the presenter goes to the 3rd slide And I go to the 3rd slide Then I should see @jefferson's tweet along with his avatar
Then %r{I should see @([']+)'s tweet along with (?:his|her) avatar$} do |user|
tweet = FakeTwitter.tweets_from(user).first browser.should contain(tweet['text'], :wait => 10) browser.should have_image(:src, tweet['profile_image_url'])end
Timeout
Then %r{I should see @([']+)'s tweet along with (?:his|her) avatar$} do |user|
tweet = FakeTwitter.tweets_from(user).first browser.should contain(tweet['text'], :wait => 10) browser.should have_image(:src, tweet['profile_image_url'])end
Spec::Matchers.define :contain do |text, options| match do |browser| options[:wait] ||= 0 browser.wait_until(options[:wait]) do browser.text.include?(text) end endend
Then %r{I should see @([']+)'s tweet along with (?:his|her) avatar$} do |user|
tweet = FakeTwitter.tweets_from(user).first browser.should contain(tweet['text'], :wait => 10) browser.should have_image(:src, tweet['profile_image_url'])end
Spec::Matchers.define :contain do |text, options| match do |browser| options[:wait] ||= 0 browser.wait_until(options[:wait]) do browser.text.include?(text) end endend
Keep trying after sleepinguntil it times out
RSpec Cycle
Scenario: winner is displayed When the following tweets are tweeted that match the '#free_stuff "aaron burr"' search | From User | Text | Created At | | @adams | Aaron Burr shot Alexander Hamilton #free_stuff | 1 minute ago | | @jefferson | Aaron Burr shot Alexander Hamilton #free_stuff | 2 minutes ago | And the presenter goes to the 3rd slide And I go to the 3rd slide
Then I should see @jefferson's tweet along with his avatar
Demo!
More tricks...
Scenario: view members list Given the following wishes exist | Wish | Family Member | | Laptop | Thomas | | Nintendo Wii | Candace | | CHEEZBURGER | FuzzBuzz | When I view the wish list for "Candace"
Then I should see the following wishes | Wish | | Nintendo Wii |
Given the following wishes exist | Wish | Family Member | | Laptop | Thomas | | Nintendo Wii | Candace | | CHEEZBURGER | FuzzBuzz |
features/step_definitions/wish_steps.rb
Given /^the following wishes exist$/ do |table|
endend
| Wish | Family Member | features/step_definitions/wish_steps.rb
Given /^the following wishes exist$/ do |table| table.hashes.each do |row| member = User.find_by_name(row["Family Member"]) || create_user(:name => row["Family Member"])
member.wishes.create!(:name => row["Wish"]) endend
Given the following wishes exist | Wish | Family Member | | Laptop | Thomas | | Nintendo Wii | Candace | | CHEEZBURGER | FuzzBuzz |
Table Diffinghttp://wiki.github.com/aslakhellesoy/cucumber/multiline-step-arguments
Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers
Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen
Scenarios: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |
<input_1> <input_2> <button> <output>
| input_1 | input_2 | button | output |
Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers
Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen
Scenarios: addition | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | Scenarios: subtraction | 0 | 40 | minus | -40 |
Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers
Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen
Scenarios: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |
<input_1> <input_2> <button> <output>
| 20 | 30 | add | 50 |
<input_1> <input_2> <button> <output>
| 2 | 5 | add | 7 |
Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers
Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen
Scenarios: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |
<input_1> <input_2> <button> <output>
| 0 | 40 | add | 40 |
Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers
Scenario Outline: Add two numbers Given I have entered <input_1> into the calculator And I have entered <input_2> into the calculator When I press <button> Then the result should be <output> on the screen
Scenarios: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |
When /^I view the wish list for "(.+)"$/ do |user_name| Given "I am logged in" visit "/wishes/#{user_name}"end
Steps Within Steps
When /^I view the wish list for "(.+)"$/ do |user_name| visit "/wishes/#{user_name}"end
Steps Within Steps Given "I am logged in"
HooksBefore doend
After do |scenario|end
World doend
World(MyModule)World(HerModule)
Tagged HooksBefore('@im_special', '@me_too') do @icecream = trueend
@me_tooFeature: Lorem Scenario: Ipsum Scenario: Dolor
Feature: Sit @im_special Scenario: Amet Scenario: Consec
Spork
http://github.com/timcharper/spork
Sick of slow loading times? Spork will load your main environment once. It then runs a DRB server so cucumber (or RSpec) can run against it with the --drb flag. For each test run Spork forks a child process to run them in a clean memory state. So.. it is a DRb server that forks.. hence Spork. :)
Drinking the Cucumber Kool-Aid?
Integration tests are a scam
http://www.jbrains.ca/permalink/239
J. B. Rainsberger
Obviously, I don’t agree with this 100%. But he has some valid points. Integrations tests are not a replacement for good unit tests. Use cucumber for happy paths. Use lower level tests for design and to isolate object behavior.
Cucumber is a good hammer
Cucumber is a good hammer
Not everything is a nail
I can skp teh unit testz?
Acceptance Tests
Application LevelFor CustomersSlowGood confidencePrevent against regression
Unit Tests
Object Level- Isolated!For developersFAST! (should be at least)- Tighter Feedback LoopMore about design!!!!!!!!!!!!
Will need both gears! Some things are easier to test at the application level and vice-versa.
SRSLY?Model specs,Controller Specs,and view specs!?
W
M
More tests == More Maintenance
Test Value =Design +
Documentation +Defence (regression)
if test.value > test.cost Suite.add(test)end
KTHXBYE!BenMabey.com
github.com/bmabey
Twitter: bmabeyIRC: mabes