2011-02-03 LA RubyConf Rails3 TDD Workshop

86
TDD with Rails 3 TDD with Rails 3 Wolfram Arnold Wolfram Arnold @wolframarnold @wolframarnold www.rubyfocus.biz www.rubyfocus.biz In collaboration with: In collaboration with: LA Ruby Conference LA Ruby Conference

description

Rails 3 with TDD workshop taught at LA RubyConf 2011

Transcript of 2011-02-03 LA RubyConf Rails3 TDD Workshop

Page 1: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 2: 2011-02-03 LA RubyConf Rails3 TDD Workshop

IntroductionIntroduction

Page 3: 2011-02-03 LA RubyConf Rails3 TDD Workshop

What?

Page 4: 2011-02-03 LA RubyConf Rails3 TDD Workshop

What?What?

● Why TDD?● Rails 3 & TDD

– what's changed?

– RSpec 2

● Testing in Layers● TDD'ing model development● Factories, mocks, stubs...● Controllers & Views

Page 5: 2011-02-03 LA RubyConf Rails3 TDD Workshop

How?

Page 6: 2011-02-03 LA RubyConf Rails3 TDD Workshop

How?How?

● Presentation● Live coding demos● In-class exercises

– Pair programming

● Material from current development practice● Fun

Page 7: 2011-02-03 LA RubyConf Rails3 TDD Workshop

It works best, when...It works best, when...

Active participation

Try something new

Team Effort

Pairing

Page 8: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Efficient RailsTest-Driven

Development

Page 9: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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.

Page 10: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 11: 2011-02-03 LA RubyConf Rails3 TDD Workshop

TDD: Keeping cost of change lowTDD: Keeping cost of change low

Cost per change

Time

withTDD

withoutTDD

Page 12: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 13: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 14: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Cost of TestingCost of Testing

Model

Controller

Views Helpers

Routes

Application, Server

Application, Browser UI

Relationship to data

Cost

mostremoved

closest

Page 15: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Best ROI for TestingBest ROI for Testing

Model

Controller

Views Helpers

Routes

Application, Server

Application, Browser UI

Layers

Impact/Line of Test Code

Page 16: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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!

Page 17: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 18: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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)

Page 19: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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)

Page 20: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Know YourKnow Your

ToolsTools

Page 21: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 22: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 23: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 24: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Rails 3: rails commandRails 3: rails command

● Replaces script/*– new

– console

– dbconsole

– generate

– server

Page 25: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Let's do some codingLet's do some coding

Demo

Page 26: 2011-02-03 LA RubyConf Rails3 TDD Workshop

> rails generate rspec:install

> rails generate model User first_name:string last_name:string email:string

Page 27: 2011-02-03 LA RubyConf Rails3 TDD Workshop

TDD CycleTDD Cycle

● Start user story● Experiment● Write test● Write code● Refactor● Finish user story

Page 28: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Structure of TestsStructure of Tests

Setup

Expected value

Actual value

Verification: actual == expected?

Teardown

Page 29: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Good Tests are...Good Tests are...

Compact

Responsible for testing one concern only

Fast

DRY

Page 30: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 31: 2011-02-03 LA RubyConf Rails3 TDD Workshop

RSpec StructureRSpec Structure

before, before(:each), before(:all)

after, after(:each), after(:all)

describe do...end, nested

it do... end

Page 32: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 33: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 34: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Models: What to test?Models: What to test?

Validation Rules

Associations

Any custom method

Association Proxy Methods

Page 35: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Let's do some codingLet's do some coding

Exercise...

Page 36: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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.

Page 37: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 38: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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)

Page 39: 2011-02-03 LA RubyConf Rails3 TDD Workshop

ModelsModelsWhat to test?What to test?

Page 40: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Test Models for...Test Models for...

● validation● side-effects before/after saving● associations● association proxy methods● scopes, custom finders● nested attributes● observers● custom methods

Page 41: 2011-02-03 LA RubyConf Rails3 TDD Workshop

valid?

Page 42: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 43: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Check for Side EffectsCheck for Side Effects

Page 44: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

...

Page 45: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 46: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 47: 2011-02-03 LA RubyConf Rails3 TDD Workshop

AssociationsAssociations

Page 48: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 49: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 50: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 51: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 52: 2011-02-03 LA RubyConf Rails3 TDD Workshop

has_manyhas_many:through:through

many-to-many

relationships

Page 53: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 54: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 55: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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}

Page 56: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 57: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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”

Page 58: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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”

Page 59: 2011-02-03 LA RubyConf Rails3 TDD Workshop

ControllersControllers

Page 60: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 61: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Controller RESTful ActionsController RESTful Actions

Display methods (“Read”)

GET: index, show, new, edit

Update method

PUT

Create method

POST

Delete method

DELETE

Page 62: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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.

Page 63: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 64: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Read FormulaRead Formula

Find data, based on parameters

Assign variables

Render

Page 65: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 66: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 67: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 68: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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!

Page 69: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 70: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 71: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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”)

Page 72: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Mocks,Mocks,Doubles,Doubles,Stubs, ...Stubs, ...

Page 73: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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”)

Page 74: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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”)

Page 75: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 76: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

}

Page 77: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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)

Page 78: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Dangersof

Mocks

Page 79: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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!

Page 80: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 81: 2011-02-03 LA RubyConf Rails3 TDD Workshop

TDD withTDD with

WebservicesWebservicesAmazon RSS FeedAmazon RSS Feed

SimpleRSS gemSimpleRSS gemNokogiri XML parser gemNokogiri XML parser gem

FakeWeb mocksFakeWeb mocks

Page 82: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Step 1: Experiment

Page 83: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Step 2:Proof of Concept

Page 84: 2011-02-03 LA RubyConf Rails3 TDD Workshop

Step 3:Specs & Refactor

Page 85: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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

Page 86: 2011-02-03 LA RubyConf Rails3 TDD Workshop

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