2011-02-03 la rubyconf rails3 tdd workshop

Post on 06-May-2015

1.914 Views

Category:

Technology

1 Downloads

Preview:

Click to see full reader

DESCRIPTION

Rails 3 with TDD workshop taught at LA RubyConf 2011

TRANSCRIPT

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

top related