This document provides an overview of test-driven development (TDD) using Rails 3. It discusses why TDD is important, how to structure tests in different layers (model, controller, etc.), and what to test for models, controllers and views. It also covers RSpec 2 and useful tools like RVM. The presentation includes live coding demos and in-class exercises on TDD.
9. 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.
10. The 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
11. TDD: Keeping cost of change low
Cost per change
without
TDD
with
TDD
Time
12. Why?
Non-TDD
Accumulates “technical debt” unchecked
Removal of technical debt carries risk
The more technical debt, the higher the risk
Existing technical debt attracts more technical debt
Like compound interest
People are most likely to do what others did before them
To break the pattern heroic discipline & coordination
required
13. Testing in Layers
Application, Browser UI Selenium 1, 2
RSpec Request, Capybara Test::Unit Integration
Application, Server
Cucumber, Webrat
RSpec RSpec
Views Helpers
Views Helpers
RSpec RSpec Test::Unit Functional
Controller Routes
Controller Routes
RSpec Test::Unit
Model
Model
14. Cost of Testing
Relationship to data
most
Application, Browser UI removed
Application, Server
Views Helpers
Controller Routes
Model closest
Cost
15. Best ROI for Testing
Layers
Application, Browser UI
Application, Server
Views Helpers
Controller Routes
Model
Impact/Line of Test Code
16. TDD & Design Patterns
Skinny Controller— ➢ Designed to move logic
Fat Model from higher to lower
DRY application layers
Scopes ➢ Following design
patterns makes testing
Proxy Associations easier
Validations ➢ Code written following
... TDD economics will
naturally converge on
these design patterns!
17. 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
18. RSpec 2
● Filters to run select tests
– RSpec.configure do |c|
c.filter_run :focus => true
end
● Model specs:
– be_a_new(Array)
● Controller specs:
– integrate_views → render_views
– assigns[:key]=val → assigns(:key,val)
(deprecated)
21. RVM
● 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
22. RVM 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
23. Installing 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
24. Rails 3: rails command
● Replaces script/*
– new
– console
– dbconsole
– generate
– server
30. RSpec 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
32. RSpec Subject
describe Address do
it “must have a street” do
a = Address.new
a.should_not be_valid
a.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” do
should_not be_valid # should is called on
# subject by default
subject.errors.on(:street).should_not be_nil
end
end
33. RSpec2
● https://github.com/rspec/rspec-rails
● http://blog.davidchelimsky.net/
● http://relishapp.com/rspec
● More modular, some API changes
Gemspec file, for Rails 3:
group :development, :test do
gem 'rspec-rails', "~> 2.0.1"
end
34. Models: What to test?
Validation Rules
Associations
Any custom method
Association Proxy Methods
36. Story 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.
37. RSpec ==, eql, equal
obj.should == 5 5 == 5
obj.should eq(5)
obj.should equal(5) 5.equal 5
obj.should be(5)
Use == or eq
Object Equality vs. Identity
Unless you know you
eql, == compare values need something else
equal, === compare objects,
classes
Warning! Do not use != with RSpec.
Use should_not instead.
42. 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
44. Model Callbacks
Requirement: Callbacks:
Default a value before before_save
saving after_save
Send an email after after_destroy
saving
...
Post to a URL on delete
...
45. 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
46. How are Callbacks triggered?
Callback Trigger event
before_validation valid?
after_validation valid?
before_save save, create
after_save save, create
before_create create
after_create create
before_destroy destroy
after_destroy destroy
after_find (see docs) find
after_initialize (see docs) new
48. Model Associations
Requirement: has_many
Entities have has_one
relationships
belongs_to
Given an object, I want
to find all related
objects
has_many :through
49. Tables and Associations
class Customer < AR::Base class Order < AR::Base
has_many :orders belongs_to :customer
... ...
end end
Source: Rails Guides, http://guides.rubyonrails.org/association_basics.html
50. Migrations and Associations
create_table :addresses do |t| class Address < AR::Base
t.belongs_to :person belongs_to :person
# same as: ...
# t.integer :person_id
end
...
end
class Person < AR::Base
create_table :people do |t| has_many :addresses
...
end ...
end
53. Indices 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
54. 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
56. 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 result
it “sorts addresses by zip” do
p = Factory(:person)
# Factory for addrs with zip 23456, 12345
Address.all.should == [addr1, addr2]
p.addresses.should == [addr2, addr1]
p.should respond_to(:addresses)
end
57. More Association Options
Joins
has_many :popular_items,
:class_name => “Item”,
:include => :orders,
:group => “orders.customer_id”,
:order => “count(orders.customer_id) DESC”
58. Exercise
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”
60. Controllers
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
62. 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.
63. RESTful 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
65. Reads 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
66. Create/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
67. Create/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
68. 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!
69. 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
70. View 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
71. RSpec 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”)
73. Object level
All three create a “mock”
object.
mock(), stub(), double() at
m = mock(“A Mock”)
the Object level are
synonymous m = stub(“A Mock”)
m = double(“A Mock”)
Name for error reporting
74. Using Mocks
Mocks can have method m = mock(“A Mock”)
stubs. m.stub(:foo)
They can be called like m.foo => nil
methods.
Method stubs can return m.stub(:foo).
values. and_return(“hello”)
m.foo => “hello”
Mocks can be set up with
built-in method stubs.
m = mock(“A Mock”,
:foo => “hello”)
75. Message Expectations
Mocks can carry message m = mock(“A Mock”)
expectations.
should_receive expects a m.should_receive(:foo)
single call by default
Message expectations can m.should_receive(:foo).
return values. and_return(“hello”)
Can expect multiple calls. m.should_receive(:foo).
twice
m.should_receive(:foo).
exactly(5).times
77. Partial Mocks
jan1 =
Time.civil(2010)
Replace a method on an
existing class. Time.stub!(:now).
and_return(jan1)
Add a method to an
Time.stub!(:jan1).
existing class.
and_return(jan1)
79. Problems
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!
80. 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
81. TDD with
Webservices
Amazon RSS Feed
SimpleRSS gem
Nokogiri XML parser gem
FakeWeb mocks
85. Exercise: 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
86. Reference
● 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