Writing Software not code With. Ben Mabey

Similar documents
Most of the security testers I know do not have

Cucumber and Capybara

Installing Ruby on Windows XP

Testing Rails. by Josh Steiner. thoughtbot

Deep in the CRUD LEVEL 1

Rails 4 Quickly. Bala Paranj.

RESTful Rails Development. translated by Florian Görsdorf and Ed Ruder

The Cucumber Book. Extracted from: Behaviour-Driven Development for Testers and Developers. The Pragmatic Bookshelf

Hudson Continous Integration Server. Stefan Saasen,

SAHARA FASHION15 RESPONSIVE MAGENTO THEME

Cucumber: Finishing the Example. CSCI 5828: Foundations of Software Engineering Lecture 23 04/09/2012

TESTING FRAMEWORKS. Gayatri Ghanakota

Comparing Dynamic and Static Language Approaches to Web Frameworks

Version Control! Scenarios, Working with Git!

Fermilab Service Desk

QUICK START GUIDE. Cloud based Web Load, Stress and Functional Testing

Improving your Drupal Development workflow with Continuous Integration

Cloudwords Drupal Module. Quick Start Guide

Achieving Continuous Integration with Drupal

How to Make a Working Contact Form for your Website in Dreamweaver CS3

5 Mistakes to Avoid on Your Drupal Website

Note: With v3.2, the DocuSign Fetch application was renamed DocuSign Retrieve.

EPiServer Operator's Guide

Guide to Automating Workflows Quickly and Easily

Brakeman and Jenkins: The Duo Detects Defects in Ruby on Rails Code

Ruby On Rails. CSCI 5449 Submitted by: Bhaskar Vaish

Intellect Platform - The Workflow Engine Basic HelpDesk Troubleticket System - A102

Ruby on Rails is a web application framework written in Ruby, a dynamically typed programming language The amazing productivity claims of Rails is

SAHARA DIGITAL8 RESPONSIVE MAGENTO THEME

Translation Proxy A New Option for Managing Multilingual Websites

-lead Grabber Business 2010 User Guide

Missouri Department of Health and Senior Services Bureau of Immunization Assessment and Assurance February 2013

Marketo Integration Setup Guide

PHP on IBM i: What s New with Zend Server 5 for IBM i

Web Framework Performance Examples from Django and Rails

Digitally Sign an InfoPath form and Submit using

School account creation guide

CS169.1x Lecture 5: SaaS Architecture and Introduction to Rails " Fall 2012"

Git Branching for Continuous Delivery

Adobe Summit 2015 Lab 718: Managing Mobile Apps: A PhoneGap Enterprise Introduction for Marketers

Continuous Delivery on AWS. Version 1.0 DO NOT DISTRIBUTE

Kentico CMS 7.0 Intranet Administrator's Guide

Kentico Content Management System (CMS) Managing Events and News Content

MAS 90 MAS 200 Tips, Tricks and Frequently Asked Questions (FAQ s) Prepared by: The Fitzgerald Group August 11, 2004

Set internet safety parental controls with Windows

How To Write A Book Purchase On An Ipad With A Bookshop On A Pcode On A Linux (Windows) On A Macbook On A Microsoft Powerbook 2 (Windows 2.

Eliminate Workflow Friction with Git

Prestashop Ship2MyId Module. Configuration Process

Developer Workshop Marc Dumontier McMaster/OSCAR-EMR

Evaluation. Chapter 1: An Overview Of Ruby Rails. Copy. 6) Static Pages Within a Rails Application

Advanced Tornado TWENTYONE Advanced Tornado Accessing MySQL from Python LAB

Continuous Integration

Building Dynamic Web 2.0 Websites with Ruby on Rails

Magic SDE Self-Service

ManageMyHealth SMS Text Message Service User Guide. Medtech32. Version 20.0 (March 2012)

by Jonathan Kohl and Paul Rogers 40 BETTER SOFTWARE APRIL

Software Development Tools

PHP Authentication Schemes

In depth study - Dev teams tooling

GETTING STARTED WITH CONTINUOUS DELIVERY. Lana wcgp.co

Drupal CMS for marketing sites

HELP DESK MANUAL INSTALLATION GUIDE

Continuous Integration

TestTrack Test Case Management Quick Start Guide

TweetAttacks Pro. User Manual

Continuous Performance Testing

Would You Like To Earn $1000 s With The Click Of A Button?

10 kanban boards and their context

CommonSpot Content Server Version 6.2 Release Notes

Part I. OpenCIT Server

Magento Theme EM0006 for Computer store

Managing User Accounts

Migration Manager v6. User Guide. Version

Agile Web Application Testing

Apollo ACC PCI Registry Export Basics

Cloud Elements! Marketing Hub Provisioning and Usage Guide!

Testing Best Practices

Installing and Sending with DocuSign for NetSuite v2.2

Building and Deploying Web Scale Social Networking Applications Using Ruby on Rails and Oracle. Kuassi Mensah Group Product Manager

Legacy Automated Testing Bridge QAComplete ALMComplete HP Quick Test Pro v

DreamFactory & Modus Create Case Study

Build it with Drupal 8

Primavera Unifier v9.14 / 2014 EPPM Day Hands On Session Exercise Document

Modern CI/CD and Asset Serving

Everyday Lessons from Rakudo Architecture. Jonathan Worthington

Installation & User Guide

BDD FOR AUTOMATING WEB APPLICATION TESTING. Stephen de Vries

ThirtySix Software WRITE ONCE. APPROVE ONCE. USE EVERYWHERE. SMARTDOCS SHAREPOINT CONFIGURATION GUIDE THIRTYSIX SOFTWARE

Qlik REST Connector Installation and User Guide

Managing User Accounts

Title Page. Informed Filler. User s Manual. Shana Corporation Avenue Edmonton, Alberta, Canada T6E 5C5

Transcription:

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