Speedy TDD with RailsReclaiming your feedback loop (the wrong way)     Creative Commons Attribution-Noncommercial-Share Al...
ierSpeedy TDD with RailsReclaiming your feedback loop (the wrong way)     Creative Commons Attribution-Noncommercial-Share...
What say Google about tests?
JavaScriptNothing unusual here
ClojureSeems pretty uneventful too
ScalaEqually unexciting
PythonTesting is pretty boring really
JavaYou’re probably ok as long as you speak German
C#More concerned with recruitment than code
FortranYou’re on your own
DjangoSince, in fairness, we’re testing web apps here
RailsYou can hear their pained cries for help
Why does test speed matter?
Red        Refactor                GreenThe TDD loopThis constrains how fast you can learn whetheryour code implements the...
TDD orders of magnitude0.01s  Full-app validation on every run0.1s  Continuous development flow1s  Interrupted development ...
What is the current situation?
Rails: one testOne RSpec exampleRSpec 2.8.0 on MRI Ruby 1.9.3
Rails: 16 testsBooting Rails is the constraint
What can we do?
Attack vector 1:The Ruby Runtime
Upgrade from Ruby 1.9.2The `require` method is much faster in 1.9.3But… Heroku currently only offers 1.9.2
Switch to 1.8.7 This was seriously proposed to me on the rspec-users list Baby + bathwater?
Attack vector 2:Pre-loading and forking Rails
Spinhttps://github.com/jstorimer/spinStart the server (loads Rails): spin serveRun tests  spin push test/unit/product_test...
Guard::Spinguard spin do watch(%r{^spec/.+_spec.rb}) watch(%r{^app/(.+).rb})                   { |m| "spec/#{m[1]}_spec.rb...
Guard::SpinFaster, but not an order of magnitude fasterOutcome: only worth it if it causes no problems
Sporkhttps://github.com/sporkrb/sporkRequires changing spec_helper.rbUses enough metaprogramming monkey-patching ofRails t...
Spork monkey-patchingdef preload_rails if deprecated_version && (not /^3/.match(deprecated_version))   puts "This version ...
Spork monkey-patching…AbstractController::Helpers::ClassMethods.module_eval do def helper(*args, &block)  ([args].flatten -...
Forking Rails? Only saves part of the time Introduces potential problems   Reloading the pre-fork   Hacks to make it work ...
Attack vector 3:Hijack code reloading forbrowser integration tests
Code reloadingUsed in the development environmentCalled cache_classes  This is a lie! Ruby does not have real code reloadi...
Guard::Rails# Running with `daemon: true` because I cant figure out how to turn off enough Rails loggingguard "rails", envi...
environments/acceptance.rb  MyApp::Application.configure do   config.cache_classes = false   config.consider_all_requests_loc...
capybara-webkit testsFeedback time comparable to controller tests
Code reloading summarySpeeds up start time of browser tests considerablyTests must be written to work cross-processFewer i...
Attack vector 4:Split the tests by dependency
Example: Mongoid  require spec_helper  require spec/environments/mongoid  require_unless_rails_loaded app/models/question ...
spec/environments/mongoid    require_relative common    # Gem dependencies    require mongoid    if !rails_loaded?      EN...
spec/environments/common       def require_unless_rails_loaded(file)        require(file) unless rails_loaded?       end    ...
spec_helper.rb       def rails_loaded?        Object.const_defined?(:MyApp) &&         MyApp.const_defined?(:Application)   ...
Guardfile (multiple guards)  guard "rspec", spec_paths: %w[ spec/controllers ], cli: "--color --format Fuubar" do   watch(s...
Running in GuardMongoid tests now running ~1s not ~10sNot brilliant, but an order of magnitude change
Concluding thoughts
How much does this help?Bypassing the Rails boot process can increasefeedback speed by an order of magnitudeWithout changi...
Is this the right way? No. Tune in next month for “Speedy TDD in Rails (the righter way”!
Upcoming SlideShare
Loading in...5
×

Speedy TDD with Rails

6,048

Published on

Slides from my presentation at ShRUG (Sheffield Ruby User Group) January 2012 http://shrug.org/meetings/shrug-26/

Published in: Technology
2 Comments
5 Likes
Statistics
Notes
  • Heh, actually it was never me planning to write this. The one you're looking for is Tom Crayford's 'Isolation vs Rails: More Fastererer Speedy Testing Mk II Edition' talk, available here: http://shrug.org/meetings/shrug-27/

    This is well worth a look, and is the 'right way' (it's how I write code when I'm not forced to couple it to Rails) but you'd have to ask Tom where the slides are.

    Thanks for you comment, and glad you liked the presentation!
    Ash
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
  • Good stuff, thanks. Did you ever make the follow-up presentation?
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
No Downloads
Views
Total Views
6,048
On Slideshare
0
From Embeds
0
Number of Embeds
5
Actions
Shares
0
Downloads
26
Comments
2
Likes
5
Embeds 0
No embeds

No notes for slide
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Transcript of "Speedy TDD with Rails"

    1. 1. Speedy TDD with RailsReclaiming your feedback loop (the wrong way) Creative Commons Attribution-Noncommercial-Share Alike Ash Moran 2.0 UK: England & Wales License PatchSpace Ltd
    2. 2. ierSpeedy TDD with RailsReclaiming your feedback loop (the wrong way) Creative Commons Attribution-Noncommercial-Share Alike Ash Moran 2.0 UK: England & Wales License PatchSpace Ltd
    3. 3. What say Google about tests?
    4. 4. JavaScriptNothing unusual here
    5. 5. ClojureSeems pretty uneventful too
    6. 6. ScalaEqually unexciting
    7. 7. PythonTesting is pretty boring really
    8. 8. JavaYou’re probably ok as long as you speak German
    9. 9. C#More concerned with recruitment than code
    10. 10. FortranYou’re on your own
    11. 11. DjangoSince, in fairness, we’re testing web apps here
    12. 12. RailsYou can hear their pained cries for help
    13. 13. Why does test speed matter?
    14. 14. Red Refactor GreenThe TDD loopThis constrains how fast you can learn whetheryour code implements the specification
    15. 15. TDD orders of magnitude0.01s Full-app validation on every run0.1s Continuous development flow1s Interrupted development flow10s Check Twitter more than you check your app
    16. 16. What is the current situation?
    17. 17. Rails: one testOne RSpec exampleRSpec 2.8.0 on MRI Ruby 1.9.3
    18. 18. Rails: 16 testsBooting Rails is the constraint
    19. 19. What can we do?
    20. 20. Attack vector 1:The Ruby Runtime
    21. 21. Upgrade from Ruby 1.9.2The `require` method is much faster in 1.9.3But… Heroku currently only offers 1.9.2
    22. 22. Switch to 1.8.7 This was seriously proposed to me on the rspec-users list Baby + bathwater?
    23. 23. Attack vector 2:Pre-loading and forking Rails
    24. 24. Spinhttps://github.com/jstorimer/spinStart the server (loads Rails): spin serveRun tests spin push test/unit/product_test.rb spin push a_test.rb b_test.rb …No changes to the app whatsoeverInternally does some jiggery-pokery for RSpec
    25. 25. Guard::Spinguard spin do watch(%r{^spec/.+_spec.rb}) watch(%r{^app/(.+).rb}) { |m| "spec/#{m[1]}_spec.rb" } watch(%r{^app/(.+).haml}) { |m| "spec/#{m[1]}.haml_spec.rb" } watch(%r{^lib/(.+).rb}) { |m| "spec/lib/#{m[1]}_spec.rb" } watch(%r{^app/controllers/(.+)_(controller).rb}) { |m| [ "spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/requests/#{m[1]}_spec.rb" ] }end
    26. 26. Guard::SpinFaster, but not an order of magnitude fasterOutcome: only worth it if it causes no problems
    27. 27. Sporkhttps://github.com/sporkrb/sporkRequires changing spec_helper.rbUses enough metaprogramming monkey-patching ofRails to summon a Greater Uncle Bob Daemon https://github.com/sporkrb/spork/blob/master/lib/ spork/app_framework/rails.rb#L43-80
    28. 28. Spork monkey-patchingdef preload_rails if deprecated_version && (not /^3/.match(deprecated_version)) puts "This version of spork only supports Rails 3.0. To use spork with rails 2.3.x, downgrade to spork 0.8.x." exit 1 end require application_file ::Rails.application ::Rails::Engine.class_eval do def eager_load! # turn off eager_loading, all together end end # Spork.trap_method(::AbstractController::Helpers::ClassMethods, :helper) Spork.trap_method(::ActiveModel::Observing::ClassMethods, :instantiate_observers) Spork.each_run { ActiveRecord::Base.establish_connection rescue nil } if Object.const_defined?(:ActiveRecord) …
    29. 29. Spork monkey-patching…AbstractController::Helpers::ClassMethods.module_eval do def helper(*args, &block) ([args].flatten - [:all]).each do |arg| next unless arg.is_a?(String) filename = arg + "_helper" unless ::ActiveSupport::Dependencies.search_for_file(filename) # this error message must raise in the format such that LoadError#path returns the filename raise LoadError.new("Missing helper file helpers/%s.rb" % filename) end end Spork.each_run(false) do modules_for_helpers(args).each do |mod| add_template_helper(mod) end _helpers.module_eval(&block) if block_given? end endend
    30. 30. Forking Rails? Only saves part of the time Introduces potential problems Reloading the pre-fork Hacks to make it work Can introduce many subtle bugs Doesn’t address the real problem: depending on Rails
    31. 31. Attack vector 3:Hijack code reloading forbrowser integration tests
    32. 32. Code reloadingUsed in the development environmentCalled cache_classes This is a lie! Ruby does not have real code reloading like ErlangCan be used to speed up browser integration tests Called “acceptance” here, which may not be true
    33. 33. Guard::Rails# Running with `daemon: true` because I cant figure out how to turn off enough Rails loggingguard "rails", environment: "acceptance", server: :thin, port: 3100, daemon: true do watch("Gemfile.lock") watch(%r{^(config|lib)/.*})endguard "rspec", spec_paths: %w[ spec/acceptance ], cli: "--color --format Fuubar" do watch(%r{^spec/acceptance/.+_spec.rb$})end
    34. 34. environments/acceptance.rb MyApp::Application.configure do config.cache_classes = false config.consider_all_requests_local = true config.active_support.deprecation = :log config.assets.compress = false config.action_mailer.default_url_options = { :host => localhost:3000 } config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025, :enable_starttls_auto => false } # Disable logging for now (too much noise in Guard) # I couldnt figure out how to make it log so running this as a daemon instead # config.logger = nil # config.action_controller.logger = nil # config.action_view.logger = nil end Mongoid.configure do |config| config.logger = nil end
    35. 35. capybara-webkit testsFeedback time comparable to controller tests
    36. 36. Code reloading summarySpeeds up start time of browser tests considerablyTests must be written to work cross-processFewer issues than Spin/Spork (lesser of two evils)No help at all speeding up controller testsStill doesn’t address the real problem
    37. 37. Attack vector 4:Split the tests by dependency
    38. 38. Example: Mongoid require spec_helper require spec/environments/mongoid require_unless_rails_loaded app/models/question require_unless_rails_loaded app/models/user require spec/support/blueprints/question describe Question do describe "#populate" do let(:source_question) { Question.make(value: "Submetric 1a Q1", comment: "Submetric 1a A1") } let(:target_question) { Question.make(value: "this does not get overwritten", comment: "this gets overwritten") } before(:each) do source_question.populate(target_question) end subject { target_question } its(:value) { should be == "this does not get overwritten" } its(:comment) { should be == "Submetric 1a A1" } end end
    39. 39. spec/environments/mongoid require_relative common # Gem dependencies require mongoid if !rails_loaded? ENV["RACK_ENV"] ||= "test" # Hack to use the Mongoid.load! Mongoid.load!("config/mongoid.yml") Mongoid.configure do |config| config.logger = nil end end RSpec.configure do |config| config.before(:each) do Mongoid::IdentityMap.clear end end
    40. 40. spec/environments/common def require_unless_rails_loaded(file) require(file) unless rails_loaded? end This may be paranoia
    41. 41. spec_helper.rb def rails_loaded? Object.const_defined?(:MyApp) && MyApp.const_defined?(:Application) end
    42. 42. Guardfile (multiple guards) guard "rspec", spec_paths: %w[ spec/controllers ], cli: "--color --format Fuubar" do watch(spec/environments/rails.rb) { "spec/controllers" } watch(%r{^spec/controllers/.+_spec.rb$}) watch(%r{^app/controllers/(.+).rb$}) { |m| "spec/#{m[1]}_spec.rb" } end guard "rspec", spec_paths: %w[ spec/models ], cli: "--color --format Fuubar" do watch(spec/spec_helper.rb) { "spec/models" } watch(spec/environments/mongoid.rb) { "spec/models" } watch(%r{^spec/support/(.+).rb$}) { "spec/models" } watch(%r{^spec/models/.+_spec.rb$}) watch(%r{^app/models/(.+).rb$}) { |m| "spec/#{m[1]}_spec.rb" } watch(%r{^app/models/concerns.rb$}) { |m| "spec/models" } end
    43. 43. Running in GuardMongoid tests now running ~1s not ~10sNot brilliant, but an order of magnitude change
    44. 44. Concluding thoughts
    45. 45. How much does this help?Bypassing the Rails boot process can increasefeedback speed by an order of magnitudeWithout changing your code, you can turn a nightmareinto a bad dreamThe size of the gains depend on how manydependencies you have left
    46. 46. Is this the right way? No. Tune in next month for “Speedy TDD in Rails (the righter way”!
    1. A particular slide catching your eye?

      Clipping is a handy way to collect important slides you want to go back to later.

    ×