2011-02-03 la rubyconf rails3 tdd workshop
DESCRIPTION
Rails 3 with TDD workshop taught at LA RubyConf 2011TRANSCRIPT
TDD with Rails 3TDD with Rails 3
Wolfram ArnoldWolfram Arnold@wolframarnold@wolframarnold
www.rubyfocus.bizwww.rubyfocus.biz
In collaboration with:In collaboration with:LA Ruby ConferenceLA Ruby Conference
IntroductionIntroduction
What?
What?What?
● Why TDD?● Rails 3 & TDD
– what's changed?
– RSpec 2
● Testing in Layers● TDD'ing model development● Factories, mocks, stubs...● Controllers & Views
How?
How?How?
● Presentation● Live coding demos● In-class exercises
– Pair programming
● Material from current development practice● Fun
It works best, when...It works best, when...
Active participation
Try something new
Team Effort
Pairing
Efficient RailsTest-Driven
Development
Why “efficient” and “testing”?Why “efficient” and “testing”?
“Testing takes too much time.”
“It's more efficient to test later.”
“Testing is the responsibility of QA, not developers.”
“It's not practical to test X.”
“Tests keep breaking too often.”
When data changes.
When UI design changes.
The Role of TestingThe Role of Testing
Development without tests...
fails to empower developers to efficiently take responsibility for quality of the code delivered
makes collaboration harder
build narrow silos of expertise
instills fear & resistance to change
makes documentation a chore
stops being efficient very soon
TDD: Keeping cost of change lowTDD: Keeping cost of change low
Cost per change
Time
withTDD
withoutTDD
Why?Why?
Non-TDD
Accumulates “technical debt” unchecked
Removal of technical debt carries riskThe more technical debt, the higher the risk
Existing technical debt attracts more technical debtLike compound interest
People are most likely to do what others did before them
To break the pattern heroic discipline & coordination required
Testing in LayersTesting in Layers
Model RSpecModel
Controller RSpecController
Views RSpecHelpers
Helpers RSpecViews
Routes
Test::Unit
Test::Unit FunctionalRSpecRoutes
Application, Server
Application, Browser UI
RSpec Request, CapybaraCucumber, Webrat
Selenium 1, 2
Test::Unit Integration
Cost of TestingCost of Testing
Model
Controller
Views Helpers
Routes
Application, Server
Application, Browser UI
Relationship to data
Cost
mostremoved
closest
Best ROI for TestingBest ROI for Testing
Model
Controller
Views Helpers
Routes
Application, Server
Application, Browser UI
Layers
Impact/Line of Test Code
TDD & Design PatternsTDD & Design Patterns
Skinny Controller—Fat Model
DRY
Scopes
Proxy Associations
Validations
...
➢ Designed to move logic from higher to lower application layers
➢ Following design patterns makes testing easier
➢ Code written following TDD economics will naturally converge on these design patterns!
Rails 3 – what's new?Rails 3 – what's new?
● gem management with bundler
● scripts: rails g, s, ...
● constants: RAILS_ENV → Rails.env...
● errors.on(:key) → errors[:key], always Array now
● routes: match '/' => 'welcome#index'
● configuration in application.rb
● ActiveRecord: Scopes, Relations, Validations
● Controllers: no more verify
● ActionMailer: API overhaul
● Views: auto-escaped, unobtrusive JS
RSpec 2RSpec 2
● Filters to run select tests– RSpec.configure do |c|
c.filter_run :focus => trueend
● Model specs:– be_a_new(Array)
● Controller specs:– integrate_views → render_views
– assigns[:key]=val → assigns(:key,val)(deprecated)
RSpec 2 cont'dRSpec 2 cont'd
● View specs:– response → rendered
– assigns[:key]=val → assign(:key, val) (Req)
● Routing specs:– route_for is gone
– route_to, be_routable (also in Rspec 1.3)
Know YourKnow Your
ToolsTools
RVMRVM
● multiple, isolated Rubies● can have different gemsets each
Install: http://rvm.beginrescueend.com/rvm/install/
As User or System-Wide
> rvm install ruby-1.8.7
> rvm gemset create rails3
> rvm ruby-1.8.7@rails3
> rvm info
RVM SettingsRVM Settings
● System: /etc/rvmrc● User: ~/.rvmrc● Project: .rvmrc in project(s) root
> mkdir workspace
> cd workspace
> echo “ruby-1.8.7@rails3” > .rvmrc
> cd ../workspace
> rvm info
> gem list
Installing gemsInstalling gems
● Do NOT use sudo with RVM!!!● gems are specific to the Ruby and the gemset
> rvm info → make sure we're on gemset “rails3”
> gem install rails
> gem install rspec-rails
> gem list
Rails 3: rails commandRails 3: rails command
● Replaces script/*– new
– console
– dbconsole
– generate
– server
Let's do some codingLet's do some coding
Demo
> rails generate rspec:install
> rails generate model User first_name:string last_name:string email:string
TDD CycleTDD Cycle
● Start user story● Experiment● Write test● Write code● Refactor● Finish user story
Structure of TestsStructure of Tests
Setup
Expected value
Actual value
Verification: actual == expected?
Teardown
Good Tests are...Good Tests are...
Compact
Responsible for testing one concern only
Fast
DRY
RSpec VerificationsRSpec Verifications
should respond_to
should be_nil
→ works with any ? method (so-called “predicates”)
should be_valid
should_not be_nil; should_not be_valid
lambda {...}.should change(), {}, .from().to(), .by()
should ==, equal, eq, be
RSpec StructureRSpec Structure
before, before(:each), before(:all)
after, after(:each), after(:all)
describe do...end, nested
it do... end
RSpec SubjectRSpec Subject
describe Address do
it “must have a street” doa = Address.newa.should_not be_valida.errors.on(:street).should_not be_nil
end
#subject { Address.new } # Can be omitted if .new # on same class as in describe
it “must have a street” doshould_not be_valid # should is called on
# subject by defaultsubject.errors.on(:street).should_not be_nil
end
end
RSpec2RSpec2
● https://github.com/rspec/rspec-rails● http://blog.davidchelimsky.net/● http://relishapp.com/rspec● More modular, some API changesGemspec file, for Rails 3:
group :development, :test do
gem 'rspec-rails', "~> 2.0.1"
end
Models: What to test?Models: What to test?
Validation Rules
Associations
Any custom method
Association Proxy Methods
Let's do some codingLet's do some coding
Exercise...
Story Exercise #1Story Exercise #1
A User object must have a first and last name.
A User object can construct a full name from the first and last name.
A User object has an optional middle name.
A User object returns a full name including, if present, the middle name.
RSpec ==, eql, equalRSpec ==, eql, equal
obj.should == 5
obj.should eq(5)
obj.should equal(5)
obj.should be(5)
5 == 5
5.equal 5
Object Equality vs. Identity
eql, == compare values
equal, === compare objects,classes
Warning! Do not use != with RSpec.Use should_not instead.
Use == or eqUnless you know you need something else
RSpec should changeRSpec should change
lambda {…}.should change...expect {…}.to change...
expect {Person.create
}.to change(Person, :count).from(0).to(1)
lambda {@bob.addresses.create(:street => “...”)
}.should change{@bob.addresses.count}.by(1)
ModelsModelsWhat to test?What to test?
Test Models for...Test Models for...
● validation● side-effects before/after saving● associations● association proxy methods● scopes, custom finders● nested attributes● observers● custom methods
valid?
How to Test for Validations?How to Test for Validations?
it 'requires X' do
n = Model.new
n.should_not be_valid
n.errors[:x].should_not be_empty
end
● Instantiate object with invalid property● Check for not valid?● Check for error on right attribute
Check for Side EffectsCheck for Side Effects
Model CallbacksModel Callbacks
Requirement:
Default a value before saving
Send an email after saving
Post to a URL on delete
...
Callbacks:
before_save
after_save
after_destroy
...
How to test Callbacks?How to test Callbacks?
Through their Side Effects:● Set up object in state before callback● Trigger callback● Check for side effect
it 'encrypts password on save' do
n = User.new
n.should_not be_valid
n.errors.on(:x).should_not be_nil
end
How are Callbacks triggered?How are Callbacks triggered?
Callbackbefore_validation
after_validation
before_save
after_save
before_create
after_create
before_destroy
after_destroy
after_find (see docs)
after_initialize (see docs)
Trigger eventvalid?
valid?
save, create
save, create
create
create
destroy
destroy
find
new
AssociationsAssociations
Model AssociationsModel Associations
Requirement:
Entities have relationships
Given an object, I want to find all related objects
has_many
has_one
belongs_to
has_many :through
Tables and AssociationsTables and Associations
Source: Rails Guides, http://guides.rubyonrails.org/association_basics.html
class Customer < AR::Base
has_many :orders
...
end
class Order < AR::Base
belongs_to :customer
...
end
Migrations and AssociationsMigrations and Associations
class Address < AR::Base
belongs_to :person
...
end
class Person < AR::Base
has_many :addresses
...
end
create_table :addresses do |t|
t.belongs_to :person
# same as:# t.integer :person_id...
end
create_table :people do |t|...
end
Association MethodsAssociation Methods
has_many :assets
.assets
.assets <<
.assets = [...]
.assets.delete(obj,..)
.assets.clear
.assets.empty?
.assets.create(...)
.assets.build(...)
.assets.find(...)
belongs_to :person
.person
.person =
.build_person()
.create_person()
Source: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
has_manyhas_many:through:through
many-to-many
relationships
Indices for AssociationsIndices for Associations
Rule: Any database column that can occur in a WHERE clause should have an index
create_table :addresses do |t|
t.belongs_to :person
# same as:# t.integer :person_id...
end
add_index :addresses, :person_id
How to test for Associations?How to test for Associations?
● Are the association methods present?● Checking for one is enough.● No need to “test Rails” unless using
associations with options● Check that method runs, if options used
it “has many addresses” do
p = Person.new
p.should respond_to(:addresses)
end
Association OptionsAssociation Options
Ordering
has_many :people, :order => “last_name ASC”
Class Name
belongs_to :customer, :class_name => “Person”
Foreign Key
has_many :messages, :foreign_key => “recipient_id”
Conditions
has_many :unread_messages,:class_name => “Message”,:conditions => {:read_at => nil}
How to test Assn's with Options?How to test Assn's with Options?
● Set up a non-trivial data set.● Verify that it's non-trival.● Run association method having options● Verify resultit “sorts addresses by zip” do
p = Factory(:person)# Factory for addrs with zip 23456, 12345Address.all.should == [addr1, addr2]p.addresses.should == [addr2, addr1]p.should respond_to(:addresses)
end
More Association OptionsMore Association Options
Joins
has_many :popular_items,:class_name => “Item”,:include => :orders,:group => “orders.customer_id”,:order => “count(orders.customer_id) DESC”
ExerciseExercise
A User can have 0 or more Addresses.
A User's Address must have a street, city, state and zip.
A User's Address can have an optional 2-letter country code.
If the country is left blank, it should default to “US” prior to saving.
Extra Credit:
State is required only if country is “US” or “CA”
Zip must be numerical if country is “US”
ControllersControllers
ControllersControllers
Controllers are pass-through entities
Mostly boilerplate—biz logic belongs in the model
Controllers are “dumb” or “skinny”
They follow a run-of-the mill pattern:
the Controller Formula
Controller RESTful ActionsController RESTful Actions
Display methods (“Read”)
GET: index, show, new, edit
Update method
PUT
Create method
POST
Delete method
DELETE
REST?REST?
Representational State Transfer
All resource-based applications & API's need to do similar things, namely:
create, read, update, delete
It's a convention:
no configuration, no ceremony
superior to CORBA, SOAP, etc.
RESTful rsources in RailsRESTful rsources in Rails
map.resources :people (in config/routes.rb)
people_path, people_url “named route methods”
GET /people → “index” action
POST /people → “create” action
new_person_path, new_person_url
GET /people/new → “new” action
edit_person_path, edit_person_url
GET /people/:id/edit → “edit” action with ID
person_path, person_url
GET /people/:id → “show” action with ID
PUT /people/:id → “update” action with ID
DELETE /people/:id → “destroy” action with ID
Read FormulaRead Formula
Find data, based on parameters
Assign variables
Render
Reads Test PatternReads Test Pattern
Make request (with id of record if a single record)
Check Rendering
correct template
redirect
status code
content type (HTML, JSON, XML,...)
Verify Variable Assignments
required by view
Create/Update FormulaCreate/Update Formula
Update: Find record from parameters
Create: Instantiate new model object
Assign form fields parameters to model object
This should be a single line
It is a pattern, the “Controller Formula”
Save
Handle success—typically a redirect
Handle failure—typically a render
Create/Update Test PatternCreate/Update Test Pattern
Make request with form fields to be created/upd'd
Verify Variable Assignments
Verify Check Success
Rendering
Verify Failure/Error Case
Rendering
Variables
Verify HTTP Verb protection
How much test is too much?How much test is too much?
Test anything where the code deviates from defaults, e.g. redirect vs. straight up render
These tests are not strictly necessary:
response.should be_success
response.should render_template('new')
Test anything required for the application to proceed without error
Speficially variable assignments
Do test error handling code!
How much is enough?How much is enough?
Notice: No view testing so far.
Emphasize behavior over display.
Check that the application handles errors correctly
Test views only for things that could go wrong badly
incorrect form URL
incorrect names on complicated forms, because they impact parameter representation
View TestingView Testing
RSpec controllers do not render views (by default)
Test form urls, any logic and input names
Understand CSS selector syntax
View test requires set up of variables
another reason why there should only be very few variables between controller and view
some mocks here are OK
RSpec 2 View UpdateRSpec 2 View Update
● should have_tag is gone● Use webrat matchers:
– Add “webrat” to Gemfile
– Add require 'webrat/core/matchers' to spec_helper.rb
– matcher is should have_selector(“css3”)
● response is now rendered● rendered.should have_selector(“css3”)
Mocks,Mocks,Doubles,Doubles,Stubs, ...Stubs, ...
Object levelObject level
All three create a “mock” object.
mock(), stub(), double() at the Object level are synonymous
Name for error reporting
m = mock(“A Mock”)
m = stub(“A Mock”)
m = double(“A Mock”)
Using MocksUsing Mocks
Mocks can have method stubs.
They can be called like methods.
Method stubs can return values.
Mocks can be set up with built-in method stubs.
m = mock(“A Mock”)
m.stub(:foo)
m.foo => nil
m.stub(:foo).and_return(“hello”)
m.foo => “hello”
m = mock(“A Mock”, :foo => “hello”)
Message ExpectationsMessage Expectations
Mocks can carry message expectations.
should_receive expects a single call by default
Message expectations can return values.
Can expect multiple calls.
m = mock(“A Mock”)
m.should_receive(:foo)
m.should_receive(:foo).and_return(“hello”)
m.should_receive(:foo).twice
m.should_receive(:foo).exactly(5).times
Argument ExpectationsArgument Expectations
Regular expressions
Hash keys
Block
m = mock(“A Mock”)
m.should_receive(:foo).with(/ello/)
with(hash_including(:name => 'joe'))
with { |arg1, arg2|arg1.should == 'abc'arg2.should == 2
}
Partial MocksPartial Mocks
Replace a method on an existing class.
Add a method to an existing class.
jan1 = Time.civil(2010)
Time.stub!(:now).and_return(jan1)
Time.stub!(:jan1).and_return(jan1)
Dangersof
Mocks
ProblemsProblems
Non-DRY
Simulated API vs. actual API
Maintenance
Simulated API gets out of sync with actual API
Tedious to remove after “outside-in” phase
Leads to testing implementation, not effect
Demands on integration and exploratory testing higher with mocks.
Less value per line of test code!
So what are they good for?So what are they good for?
External services
API's
System services
Time
I/O, Files, ...
Sufficiently mature (!) internal API's
Slow queries
Queries with complicated data setup
TDD withTDD with
WebservicesWebservicesAmazon RSS FeedAmazon RSS Feed
SimpleRSS gemSimpleRSS gemNokogiri XML parser gemNokogiri XML parser gem
FakeWeb mocksFakeWeb mocks
Step 1: Experiment
Step 2:Proof of Concept
Step 3:Specs & Refactor
Exercise: Step 3Exercise: Step 3
● Using TDD techniques with– FakeWeb
– mocks
● Build up a Product model with:– a fetch class method returning an array of
Product instances
– instance methods for:● title, description, link● image_url (extracted from description)
● Refactor controller & view to use Product model
ReferenceReference
● https://github.com/wolframarnold/Efficient-TDD-Rails3
● Class Videos: http://goo.gl/Pe6jE● Rspec Book● https://github.com/rspec/rspec-rails● http://blog.davidchelimsky.net/● http://relishapp.com/rspec