Upgrading to RSpec 2 with Ruby on Rails 3

Published 15 October 2010

Following on from my \previous post about upgrading our Rails 2.3.5 application to Rails 3\ the most time consuming part was getting our 600 tests passing again.\r\n\r\nWe use Rspec (upgraded to version 2), Ruby Double (rr) for mocking and Factory Girl.\r\n\r\nHere’s the process I followed to get our recently upgraded Rails 3 app working with Rspec 2.\r\n\r\nThere’s a special place in my heart for the seemingly deliberately terse docs which come with Rspec.\r\n\r\nThe first 2 lines of the \home page\ says it all:\r\n\r\n*Overview*\r\n_Behaviour Driven Development for Ruby.\r\n\r\nand that’s it.\r\n\r\nPerhaps they’re worried about running out of space on the internet?\r\n\r\nAnyway I digress, getting my tests all working again was a bit of a mission.\r\n\r\nh2. Update the Gemfile\r\n\r\nFirst the updated Gem versions. At the bottom of my Gemfile they look like this:\r\n\r\n@@@ ruby\r\ngroup :test do\r\n gem \“rspec\”\r\n gem \“rspec-rails\”\r\n gem \“autotest\”\r\n #gem \“factory_girl\”, \“2.0.0.beta1\”\r\n gem \“factory_girl_rails\”\r\n gem \“rr\”\r\nend\r\n@@@\r\n\r\nh2. New Spec Helper file\r\n\r\nThe next step is to update to the new spec_helper.rb file:\r\n\r\n@@@ ruby\r\nENV[\“RAILS_ENV\”] ||= ‘test’\r\n\r\nrequire File.expandpath(\“../../config/environment\”, FILE)\r\nrequire ‘rspec/rails’\r\nrequire ‘thinking_sphinx/test’\r\n\r\n# Requires supporting ruby files with custom matchers and macros, etc,\r\n# in spec/support/ and its subdirectories.\r\nDir[Rails.root.join(\“spec/support//.rb\”)].each {|f| require f}\r\n\r\nRSpec.configure do |config|\r\n # == Mock Framework\r\n config.mock_with :rr\r\n\r\n # Remove this line if you’re not using ActiveRecord or ActiveRecord fixtures\r\n config.fixture_path = \“#{::Rails.root}/spec/fixtures\”\r\n \r\n ThinkingSphinx::Test.init\r\n\r\n # If you’re not using ActiveRecord, or you’d prefer not to run each of your\r\n # examples within a transaction, remove the following line or assign false\r\n # instead of true.\r\n config.use_transactional_fixtures = true\r\nend\r\n@@@\r\n\r\nh2. Broken backwards compatibility\r\n\r\nFor apparently no good reason there’s a bunch of things which just no longer work.\r\n\r\nSometimes it’s just because they’ve decided to rename the methods.\r\n\r\nFor instance in all my controller tests I had to do a global search for integrate_views and replace with render_views_\r\n\r\nh2. Rails params bug\r\n\r\nPreviously all params in a test get/post/delete/put request would be converted to strings, just as they would appear to your controller in the real world.\r\n\r\nHowever there’s a bug in Rails 3 which means this no longer happens. So where this used to pass:\r\n\r\n@@@ ruby\r\ndescribe \“find contact\” do\r\n before { mock(Contact).find(\“1234\”) { Contact.new } } # expect a string for the ID\r\n describe \“successfully\” do\r\n before { get :show { :id => 1234 } } # integer param passed. This used to be converted to a string for me\r\n it \“should render the show view\” do\r\n response.should render_template(\“show\”)\r\n end\r\n end\r\nend\r\n@@@\r\n\r\nit now fails with:\r\n\r\n@@@\r\nunexpected method invocation:\r\nfind(1234)\r\nexpected invocations:\r\n- find(\“1234\”)\r\n@@@\r\n\r\nI had about 300 of these tests failing and I thought I was going to have to modify every one of them.\r\n\r\nInterestingly after digging through the Rails code I found the bug. Evidently this \“params massaging\” was not deliberately removed because the code is still there. It just overwrites the massaged parameters with the original parameters.\r\n\r\nSo here’s my fix. Create the file spec/support/action_controller.rb and copy in the content:\r\n\r\n@@@ ruby\r\nmodule ActionController\r\n class TestCase < ActiveSupport::TestCase\r\n module Behavior\r\n def process(action, parameters = nil, session = nil, flash = nil, http_method = ‘GET’)\r\n # Sanity check for required instance variables so we can give an\r\n # understandable error message.\r\n %w(routes @controller @request @response).each do |iv_name|\r\n if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil?\r\n raise \"#{iv_name} is nil: make sure you set it in your test's setup method.\"\r\n end\r\n end\r\n \r\n @request.recycle!\r\n @response.recycle!\r\n @controller.response_body = nil\r\n @controller.formats = nil\r\n @controller.params = nil\r\n \r\n @html_document = nil\r\n @request.env['REQUEST_METHOD'] = http_method\r\n \r\n parameters ||= {}\r\n @request.assign_parameters(routes, controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters)\r\n \r\n @request.session = ActionController::TestSession.new(session) unless session.nil?\r\n @request.session[\"flash\"] = @request.flash.update(flash || {})\r\n @request.session[\"flash\"].sweep\r\n \r\n @controller.request = @request\r\n #controller.params.merge!(parameters) # this is the offending line, which I removed\r\n build_request_uri(action, parameters)\r\n Base.class_eval { include Testing }\r\n controller.process_with_new_base_test(request, response)\r\n @request.session.delete('flash') if @request.session['flash'].blank?\r\n @response\r\n end\r\n end\r\n end\r\nend\r\n@\r\n\r\nnow all those tests should start passing again.\r\n\r\nh2. RSpec matchers\r\n\r\nA bunch of matchers seem to have been taken out now, but they're easy enough to roll your own as demonstrated \"in this video\":http://teachmetocode.com/screencasts/rspec-matchers/\r\n\r\nI wrote the include_text matcher so my old controller tests which look like this will now pass:\r\n\r\n@ ruby\r\nresponse.should include_text('Error: Your update failed')\r\n@\r\n\r\nYes, this is a view test which doesn't belong in the controller, but I'm not about to go through all my tests and remove a useful test.\r\n\r\nSo I created a new file called spec/support/include_text.rb and added this:\r\n\r\n@ ruby\r\nmodule RSpec::Rails\r\n module Matchers\r\n RSpec::Matchers.define :include_text do |text|\r\n match do |response_or_text|\r\n @content = response_or_text.respond_to?(:body) ? response_or_text.body : response_or_text\r\n @content.include?(text)\r\n end\r\n\r\n failure_message_for_should do |text|\r\n \"expected '#{content}’ to contain ‘#{text}’\"\r\n end\r\n\r\n failure_message_for_shouldnot do |text|\r\n \“expected #{@content} to not contain ‘#{text}’\”\r\n end\r\n end\r\n end\r\nend\r\n@@@\r\n\r\n\r\nh2. Factory Girl gem change\r\n\r\nIf you’re using Factory Girl, replace factory_girl in your Gemfile with factory_girl_rails_\r\n\r\n-I don’t know the rationale behind the change but cest la vie.-\r\nedit: after looking at the code it appears Factory Girl Rails is just a small wrapper around Factory Girl, so you will still need the Factory Girl gem, probably at version 2.\r\n\r\nh2. Assert difference has disappeared\r\n\r\nAnother useful test method has been inexplicably removed. A complete list of everything which has been removed / renamed would be very useful. I’m looking at you, \David Chelimsky\, to get my assert_difference and assert_no_difference assertions working again, I created the file spec/support/assert_difference.rb:\r\n\r\n@@@ ruby\r\ndef assert_difference(executable, how_many = 1, &block)\r\n before = eval(executable)\r\n yield\r\n after = eval(executable)\r\n after.should == before + how_many\r\nend\r\n\r\ndef assert_no_difference(executable, &block)\r\n before = eval(executable)\r\n yield\r\n after = eval(executable)\r\n after.should == before\r\nend\r\n@@@\r\n\r\nHey I’m getting closer, down to 72 failures.\r\n\r\nh2. Loading files in the lib directory\r\n\r\nThe ruby files in lib/ are no longer automatically loaded by Rails. So in application.rb I had to add this:\r\n\r\n@@@ ruby\r\nmodule Matchbook\r\n class Application < Rails::Application\r\n require Rails.root.join(\“lib\”, \“matchbook\”, \“delayed_jobs\”, \“delete_contacts_job.rb\”)\r\n require Rails.root.join(\“lib\”, \“matchbook\”, \“delayed_jobs\”, \“email_creator_job.rb\”)\r\n require Rails.root.join(\“lib\”, \“matchbook\”, \“auditing\”, \“contact_audit_sweeper.rb\”)\r\n\r\n …\r\n end\r\nend\r\n@@@\r\n\r\nh2. More Rspec pain\r\n\r\nI also get a few of these errors:\r\n\r\n@@@\r\n unexpected method invocation:\r\n valid?(nil)\r\n expected invocations:\r\n – valid?()\r\n@@@\r\n\r\nCome on, why aren’t they treated as equivalent now?\r\nWell since there’s only a few of them I went and manually added the nil parameter.\r\n\r\nh2. Action Mailer changes\r\n\r\n\According to the docs\ ActionMailer methods always return objects, not strings:\r\n\r\nbq. Every object in a Mail object returns an object, never a string. So Mail.body returns a Mail::Body class object, need to call encoded or decoded to get the string you want.\r\n\r\nSo where these tests used to pass\r\n\r\n@@@ ruby\r\n mail = SiteMailer.email(message, contact, {\“message\” => message_link, \“image\” => image_link, \“promo\” => promo_link})\r\n \r\n mail.body.should include_text(message_link.to_s)\r\n mail.body.should include_text(image_link.to_s)\r\n mail.body.should include_text(promo_link.to_s)\r\n@@@\r\n\r\nthey now fail. That’s easily fixed with:\r\n\r\n@@@ ruby\r\n mail = SiteMailer.email(message, contact, {\“message\” => message_link, \“image\” => image_link, \“promo\” => promo_link})\r\n\r\n body = mail.body.encoded.gsub(/=\\r\\n/, \“\”)\r\n body.should include_text(message_link.to_s)\r\n body.should include_text(image_link.to_s)\r\n body.should include_text(promo_link.to_s)\r\n@@@\r\n\r\nNote I updated the SiteMailer code to work with the new Rails mailer API. Info \in the guide.\. Ruby Double mocking with deprecations\r\n\r\nThis no longer works:\r\n\r\n@@@ ruby\r\nany_instance_of(Note, :validate => nil)\r\n@@@\r\n\r\nand also produces deprecation warnings. This isn’t the correct rr syntax anyway, so I replaced it with\r\n\r\n@@@ ruby\r\nstub.any_instanceof(Note).valid?(nil) { true }\r\n@@@\r\n\r\nh2. Display the full stacktrace\r\n\r\nRun specs with the -b option to include the full backtrace, rather than just the subset which includes your code.\r\n\r\nThis is useful for identifying problems in libraries and dependencies.\r\n\r\nh2. All tests passing\r\n\r\nWith some further code changes to work with the new Mail API, fixes to rename a reserved word (field is a no-no column name now) and a couple of other bits and pieces, all 613 of my tests now pass!