Successfully reported this slideshow.

Speedy TDD with Rails

5

Share

Loading in …3
×
1 of 46
1 of 46

More Related Content

Related Books

Free with a 14 day trial from Scribd

See all

Speedy TDD with Rails

  1. 1. Speedy TDD with Rails Reclaiming your feedback loop (the wrong way) Creative Commons Attribution-Noncommercial-Share Alike Ash Moran 2.0 UK: England & Wales License PatchSpace Ltd
  2. 2. ier Speedy TDD with Rails Reclaiming 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. JavaScript Nothing unusual here
  5. 5. Clojure Seems pretty uneventful too
  6. 6. Scala Equally unexciting
  7. 7. Python Testing is pretty boring really
  8. 8. Java You’re probably ok as long as you speak German
  9. 9. C# More concerned with recruitment than code
  10. 10. Fortran You’re on your own
  11. 11. Django Since, in fairness, we’re testing web apps here
  12. 12. Rails You can hear their pained cries for help
  13. 13. Why does test speed matter?
  14. 14. Red Refactor Green The TDD loop This constrains how fast you can learn whether your code implements the specification
  15. 15. TDD orders of magnitude 0.01s Full-app validation on every run 0.1s Continuous development flow 1s Interrupted development flow 10s Check Twitter more than you check your app
  16. 16. What is the current situation?
  17. 17. Rails: one test One RSpec example RSpec 2.8.0 on MRI Ruby 1.9.3
  18. 18. Rails: 16 tests Booting 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.2 The `require` method is much faster in 1.9.3 But… 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. Spin https://github.com/jstorimer/spin Start the server (loads Rails): spin serve Run tests spin push test/unit/product_test.rb spin push a_test.rb b_test.rb … No changes to the app whatsoever Internally does some jiggery-pokery for RSpec
  25. 25. Guard::Spin guard '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::Spin Faster, but not an order of magnitude faster Outcome: only worth it if it causes no problems
  27. 27. Spork https://github.com/sporkrb/spork Requires changing spec_helper.rb Uses enough metaprogramming monkey-patching of Rails 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-patching def 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 end end
  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 for browser integration tests
  32. 32. Code reloading Used in the development environment Called cache_classes This is a lie! Ruby does not have real code reloading like Erlang Can 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 can't figure out how to turn off enough Rails logging guard "rails", environment: "acceptance", server: :thin, port: 3100, daemon: true do watch("Gemfile.lock") watch(%r{^(config|lib)/.*}) end guard "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 couldn't 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 tests Feedback time comparable to controller tests
  36. 36. Code reloading summary Speeds up start time of browser tests considerably Tests must be written to work cross-process Fewer issues than Spin/Spork (lesser of two evils) No help at all speeding up controller tests Still 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 Guard Mongoid tests now running ~1s not ~10s Not brilliant, but an order of magnitude change
  44. 44. Concluding thoughts
  45. 45. How much does this help? Bypassing the Rails boot process can increase feedback speed by an order of magnitude Without changing your code, you can turn a nightmare into a bad dream The size of the gains depend on how many dependencies you have left
  46. 46. Is this the right way? No. Tune in next month for “Speedy TDD in Rails (the righter way”!

Editor's Notes

  • \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
  • ×