Your SlideShare is downloading. ×
2011-02-03 LA RubyConf Rails3 TDD Workshop
Upcoming SlideShare
Loading in...5

Thanks for flagging this SlideShare!

Oops! An error has occurred.

Saving this for later? Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime – even offline.
Text the download link to your phone
Standard text messaging rates apply

2011-02-03 LA RubyConf Rails3 TDD Workshop


Published on

Rails 3 with TDD workshop taught at LA RubyConf 2011

Rails 3 with TDD workshop taught at LA RubyConf 2011

Published in: Technology

  • Be the first to comment

No Downloads
Total Views
On Slideshare
From Embeds
Number of Embeds
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

No notes for slide


  • 1. WIFI:Network: cp/ball/rm User: laharborPassword: super123
  • 2. TDD with Rails 3 Wolfram Arnold @wolframarnoldwww.rubyfocus.bizIn collaboration with:LA Ruby Conference
  • 3. Introduction
  • 4. What?
  • 5. What?● Why TDD?● Rails 3 & TDD – whats changed? – RSpec 2● Testing in Layers● TDDing model development● Factories, mocks, stubs...● Controllers & Views
  • 6. How?
  • 7. How?PresentationLive coding demosIn-class exercisesA range of material from current development practiceHomeworkFluidity & adaptability
  • 8. How?● Presentation● Live coding demos● In-class exercises – Pair programming● Material from current development practice● Fun
  • 9. It works best, when...Active participationTry something newTeam Effort Pairing
  • 10. Efficient Rails Test-DrivenDevelopment
  • 11. Why “efficient” and “testing”?“Testing takes too much time.”“Its more efficient to test later.”“Testing is the responsibility of QA, not developers.”“Its not practical to test X.”“Tests keep breaking too often.” When data changes. When UI design changes.
  • 12. The Role of TestingDevelopment 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
  • 13. TDD: Keeping cost of change lowCost per change without TDD with TDD Time
  • 14. 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
  • 15. 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 FunctionalController Routes Controller Routes RSpec Test::Unit Model Model
  • 16. Cost of Testing Relationship to data most Application, Browser UI removed Application, Server Views HelpersController Routes Model closest Cost
  • 17. Best ROI for Testing Layers Application, Browser UI Application, Server Views HelpersController Routes Model Impact/Line of Test Code
  • 18. TDD & Design PatternsSkinny Controller— ➢ Designed to move logic Fat Model from higher to lowerDRY application layersScopes ➢ Following design patterns makes testingProxy Associations easierValidations ➢ Code written following... TDD economics will naturally converge on these design patterns!
  • 19. Rails 3 – whats 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
  • 20. 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. RSpec 2 contd● 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)
  • 22. Know YourTools
  • 23. RVM● multiple, isolated Rubies● can have different gemsets each Install: As User or System-Wide > rvm install ruby-1.8.7 > rvm gemset create rails3 > rvm ruby-1.8.7@rails3 > rvm info
  • 24. 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
  • 25. Installing gems● Do NOT use sudo with RVM!!!● gems are specific to the Ruby and the gemset > rvm info → make sure were on gemset “rails3” > gem install rails > gem install rspec-rails > gem list
  • 26. Rails 3: rails command● Replaces script/* – new – console – dbconsole – generate – server
  • 27. Lets do some codingDemo
  • 28. > rails generate rspec:install> rails generate model User first_name:string last_name:string email:string
  • 29. TDD Cycle● Start user story● Experiment● Write test● Write code● Refactor● Finish user story
  • 30. Structure of TestsSetupExpected valueActual valueVerification: actual == expected?Teardown
  • 31. Good Tests are...CompactResponsible for testing one concern onlyFastDRY
  • 32. RSpec Verificationsshould respond_toshould be_nil → works with any ? method (so-called “predicates”)should be_validshould_not be_nil; should_not be_validlambda {...}.should change(), {}, .from().to(), .by()should ==, equal, eq, be
  • 33. RSpec Structurebefore, before(:each), before(:all)after, after(:each), after(:all)describe do...end, nestedit do... end
  • 34. RSpec Subjectdescribe Address do it “must have a street” do a = a.should_not be_valid a.errors.on(:street).should_not be_nil end #subject { } # 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 endend
  • 35. RSpec2●●●● More modular, some API changes Gemspec file, for Rails 3: group :development, :test do gem rspec-rails, "~> 2.0.1" end
  • 36. Models: What to test?Validation RulesAssociationsAny custom methodAssociation Proxy Methods
  • 37. Lets do some codingExercise, but wait...
  • 38. Story Exercise #1A 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.
  • 39. RSpec ==, eql, equalobj.should == 5 5 == 5obj.should eq(5)obj.should equal(5) 5.equal 5obj.should be(5) Use == or eqObject Equality vs. Identity Unless you know youeql, == compare values need something elseequal, === compare objects, classes Warning! Do not use != with RSpec. Use should_not instead.
  • 40. RSpec should changelambda {…}.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)
  • 41. ModelsWhat to test?
  • 42. Test Models for...● validation● side-effects before/after saving● associations● association proxy methods● scopes, custom finders● nested attributes● observers● custom methods
  • 43. valid?
  • 44. How to Test for Validations?it requires X do n = n.should_not be_valid n.errors[:x].should_not be_emptyend● Instantiate object with invalid property● Check for not valid?● Check for error on right attribute
  • 45. Check for Side Effects
  • 46. Model CallbacksRequirement: Callbacks: Default a value before before_save saving after_save Send an email after after_destroy saving ... Post to a URL on delete ...
  • 47. How to test Callbacks?Through their Side Effects:● Set up object in state before callback● Trigger callback● Check for side effectit encrypts password on save do n = n.should_not be_valid n.errors.on(:x).should_not be_nilend
  • 48. 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
  • 49. Associations
  • 50. Model AssociationsRequirement: has_many Entities have has_one relationships belongs_to Given an object, I want to find all related objects has_many :through
  • 51. Tables and Associationsclass Customer < AR::Base class Order < AR::Base has_many :orders belongs_to :customer ... ...end endSource: Rails Guides,
  • 52. Migrations and Associationscreate_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::Basecreate_table :people do |t| has_many :addresses ...end ... end
  • 53. Association Methodsbelongs_to :person has_many :assets .person .assets .person = .assets << .build_person() .assets = [...] .create_person() .assets.delete(obj,..) .assets.clear .assets.empty? .assets.create(...) .assets.find(...)Source:
  • 54. has_many:throughmany-to-manyrelationships
  • 55. Indices for AssociationsRule: 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
  • 56. 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 usedit “has many addresses” do p = p.should respond_to(:addresses)end
  • 57. Association OptionsOrdering 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}
  • 58. How to test Assns with Options?● Set up a non-trivial data set.● Verify that its non-trival.● Run association method having options● Verify resultit “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
  • 59. More Association OptionsJoins has_many :popular_items, :class_name => “Item”, :include => :orders, :group => “orders.customer_id”, :order => “count(orders.customer_id) DESC”
  • 60. ExerciseA User can have 0 or more Addresses.A Users Address must have a street, city, state and zip.A Users 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”
  • 61. Controllers
  • 62. ControllersControllers are pass-through entitiesMostly boilerplate—biz logic belongs in the modelControllers are “dumb” or “skinny”They follow a run-of-the mill pattern: the Controller Formula
  • 63. Controller RESTful ActionsDisplay methods (“Read”) GET: index, show, new, editUpdate method PUTCreate method POSTDelete method DELETE
  • 64. REST?Representational State TransferAll resource-based applications & APIs need to do similar things, namely:create, read, update, deleteIts a convention: no configuration, no ceremony superior to CORBA, SOAP, etc.
  • 65. RESTful rsources in Railsmap.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
  • 66. Read FormulaFind data, based on parametersAssign variablesRender
  • 67. Reads Test PatternMake 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
  • 68. Create/Update FormulaUpdate: Find record from parametersCreate: Instantiate new model objectAssign form fields parameters to model object This should be a single line It is a pattern, the “Controller Formula”SaveHandle success—typically a redirectHandle failure—typically a render
  • 69. Create/Update Test PatternMake request with form fields to be created/upddVerify Variable AssignmentsVerify Check Success RenderingVerify Failure/Error Case Rendering VariablesVerify HTTP Verb protection
  • 70. How much test is too much?Test anything where the code deviates from defaults, e.g. redirect vs. straight up renderThese 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 assignmentsDo test error handling code!
  • 71. How much is enough?Notice: No view testing so far.Emphasize behavior over display.Check that the application handles errors correctlyTest views only for things that could go wrong badly incorrect form URL incorrect names on complicated forms, because they impact parameter representation
  • 72. View TestingRSpec controllers do not render views (by default)Test form urls, any logic and input namesUnderstand CSS selector syntaxView 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
  • 73. 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”)
  • 74. Mocks,Doubles,Stubs, ...
  • 75. Object levelAll 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
  • 76. Using MocksMocks can have method m = mock(“A Mock”) stubs. m.stub(:foo)They can be called like => nil methods.Method stubs can return m.stub(:foo). values. and_return(“hello”) => “hello”Mocks can be set up with built-in method stubs. m = mock(“A Mock”, :foo => “hello”)
  • 77. Message ExpectationsMocks can carry message m = mock(“A Mock”) expectations.should_receive expects a m.should_receive(:foo) single call by defaultMessage 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
  • 78. Argument Expectations m = mock(“A Mock”)Regular expressions m.should_receive(:foo). with(/ello/)Hash keys with(hash_including( :name => joe))Block with { |arg1, arg2| arg1.should == abc arg2.should == 2 }
  • 79. 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)
  • 80. Dangers of Mocks
  • 81. ProblemsNon-DRY Simulated API vs. actual APIMaintenance Simulated API gets out of sync with actual API Tedious to remove after “outside-in” phaseLeads to testing implementation, not effectDemands on integration and exploratory testing higher with mocks.Less value per line of test code!
  • 82. So what are they good for?External services APIsSystem services Time I/O, Files, ...Sufficiently mature (!) internal APIs Slow queries Queries with complicated data setup
  • 83. TDD withWebservices Amazon RSS Feed SimpleRSS gem Nokogiri XML parser gem FakeWeb mocks
  • 84. Step 1: Experiment
  • 85. Step 2:Proof of Concept
  • 86. Step 3:Specs & Refactor
  • 87. 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
  • 88. Reference● TDD-Rails3● Class Videos:● Rspec Book●●●