2011-02-03 LA RubyConf Rails3 TDD Workshop


Published on

Rails 3 with TDD workshop taught at LA RubyConf 2011

Published in: Technology
  • Be the first to comment

2011-02-03 LA RubyConf Rails3 TDD Workshop

  1. 1. WIFI:Network: cp/ball/rm User: laharborPassword: super123
  2. 2. TDD with Rails 3 Wolfram Arnold @wolframarnoldwww.rubyfocus.bizIn collaboration with:LA Ruby Conference
  3. 3. Introduction
  4. 4. What?
  5. 5. What?● Why TDD?● Rails 3 & TDD – whats changed? – RSpec 2● Testing in Layers● TDDing model development● Factories, mocks, stubs...● Controllers & Views
  6. 6. How?
  7. 7. How?PresentationLive coding demosIn-class exercisesA range of material from current development practiceHomeworkFluidity & adaptability
  8. 8. How?● Presentation● Live coding demos● In-class exercises – Pair programming● Material from current development practice● Fun
  9. 9. It works best, when...Active participationTry something newTeam Effort Pairing
  10. 10. Efficient Rails Test-DrivenDevelopment
  11. 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. 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. 13. TDD: Keeping cost of change lowCost per change without TDD with TDD Time
  14. 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. 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. 16. Cost of Testing Relationship to data most Application, Browser UI removed Application, Server Views HelpersController Routes Model closest Cost
  17. 17. Best ROI for Testing Layers Application, Browser UI Application, Server Views HelpersController Routes Model Impact/Line of Test Code
  18. 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. 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. 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. 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. 22. Know YourTools
  23. 23. 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
  24. 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. 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. 26. Rails 3: rails command● Replaces script/* – new – console – dbconsole – generate – server
  27. 27. Lets do some codingDemo
  28. 28. > rails generate rspec:install> rails generate model User first_name:string last_name:string email:string
  29. 29. TDD Cycle● Start user story● Experiment● Write test● Write code● Refactor● Finish user story
  30. 30. Structure of TestsSetupExpected valueActual valueVerification: actual == expected?Teardown
  31. 31. Good Tests are...CompactResponsible for testing one concern onlyFastDRY
  32. 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. 33. RSpec Structurebefore, before(:each), before(:all)after, after(:each), after(:all)describe do...end, nestedit do... end
  34. 34. RSpec Subjectdescribe 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 endend
  35. 35. 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
  36. 36. Models: What to test?Validation RulesAssociationsAny custom methodAssociation Proxy Methods
  37. 37. Lets do some codingExercise, but wait...
  38. 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. 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. 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. 41. ModelsWhat to test?
  42. 42. Test Models for...● validation● side-effects before/after saving● associations● association proxy methods● scopes, custom finders● nested attributes● observers● custom methods
  43. 43. valid?
  44. 44. How to Test for Validations?it requires X do n = Model.new 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. 45. Check for Side Effects
  46. 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. 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 = User.new n.should_not be_valid n.errors.on(:x).should_not be_nilend
  48. 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. 49. Associations
  50. 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. 51. Tables and Associationsclass Customer < AR::Base class Order < AR::Base has_many :orders belongs_to :customer ... ...end endSource: Rails Guides, http://guides.rubyonrails.org/association_basics.html
  52. 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. 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.build(...) .assets.find(...)Source: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
  54. 54. has_many:throughmany-to-manyrelationships
  55. 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. 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 = Person.new p.should respond_to(:addresses)end
  57. 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. 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. 59. More Association OptionsJoins has_many :popular_items, :class_name => “Item”, :include => :orders, :group => “orders.customer_id”, :order => “count(orders.customer_id) DESC”
  60. 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. 61. Controllers
  62. 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. 63. Controller RESTful ActionsDisplay methods (“Read”) GET: index, show, new, editUpdate method PUTCreate method POSTDelete method DELETE
  64. 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. 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. 66. Read FormulaFind data, based on parametersAssign variablesRender
  67. 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. 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. 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. 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. 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. 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. 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. 74. Mocks,Doubles,Stubs, ...
  75. 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. 76. Using MocksMocks 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”)
  77. 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. 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. 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. 80. Dangers of Mocks
  81. 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. 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. 83. TDD withWebservices Amazon RSS Feed SimpleRSS gem Nokogiri XML parser gem FakeWeb mocks
  84. 84. Step 1: Experiment
  85. 85. Step 2:Proof of Concept
  86. 86. Step 3:Specs & Refactor
  87. 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. 88. 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