Blog
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.
We use Rspec (upgraded to version 2), Ruby Double (rr) for mocking and Factory Girl.
Here’s the process I followed to get our recently upgraded Rails 3 app working with Rspec 2.
There’s a special place in my heart for the seemingly deliberately terse docs which come with Rspec.
The first 2 lines of the home page says it all:
Overview
Behaviour Driven Development for Ruby.
and that’s it.
Perhaps they’re worried about running out of space on the internet?
Anyway I digress, getting my tests all working again was a bit of a mission.
Update the Gemfile
First the updated Gem versions. At the bottom of my Gemfile they look like this:
group :test do gem "rspec" gem "rspec-rails" gem "autotest" #gem "factory_girl", "2.0.0.beta1" gem "factory_girl_rails" gem "rr" end
New Spec Helper file
The next step is to update to the new spec_helper.rb file:
ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'thinking_sphinx/test' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} RSpec.configure do |config| # == Mock Framework config.mock_with :rr # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" ThinkingSphinx::Test.init # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. config.use_transactional_fixtures = true end
Broken backwards compatibility
For apparently no good reason there’s a bunch of things which just no longer work.
Sometimes it’s just because they’ve decided to rename the methods.
For instance in all my controller tests I had to do a global search for integrate_views and replace with render_views
Rails params bug
Previously 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.
However there’s a bug in Rails 3 which means this no longer happens. So where this used to pass:
describe "find contact" do before { mock(Contact).find("1234") { Contact.new } } # expect a string for the ID describe "successfully" do before { get :show { :id => 1234 } } # integer param passed. This used to be converted to a string for me it "should render the show view" do response.should render_template("show") end end end
it now fails with:
unexpected method invocation:
find(1234)
expected invocations:
- find("1234")I had about 300 of these tests failing and I thought I was going to have to modify every one of them.
Interestingly 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.
So here’s my fix. Create the file spec/support/action_controller.rb and copy in the content:
module ActionController class TestCase < ActiveSupport::TestCase module Behavior def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') # Sanity check for required instance variables so we can give an # understandable error message. %w(@routes @controller @request @response).each do |iv_name| if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil? raise "#{iv_name} is nil: make sure you set it in your test's setup method." end end @request.recycle! @response.recycle! @controller.response_body = nil @controller.formats = nil @controller.params = nil @html_document = nil @request.env['REQUEST_METHOD'] = http_method parameters ||= {} @request.assign_parameters(@routes, @controller.class.name.underscore.sub(/_controller$/, ''), action.to_s, parameters) @request.session = ActionController::TestSession.new(session) unless session.nil? @request.session["flash"] = @request.flash.update(flash || {}) @request.session["flash"].sweep @controller.request = @request #@controller.params.merge!(parameters) # this is the offending line, which I removed build_request_uri(action, parameters) Base.class_eval { include Testing } @controller.process_with_new_base_test(@request, @response) @request.session.delete('flash') if @request.session['flash'].blank? @response end end end end
now all those tests should start passing again.
RSpec matchers
A bunch of matchers seem to have been taken out now, but they’re easy enough to roll your own as demonstrated in this video
I wrote the include_text matcher so my old controller tests which look like this will now pass:
response.should include_text('Error: Your update failed')Yes, 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.
So I created a new file called spec/support/include_text.rb and added this:
module RSpec::Rails module Matchers RSpec::Matchers.define :include_text do |text| match do |response_or_text| @content = response_or_text.respond_to?(:body) ? response_or_text.body : response_or_text @content.include?(text) end failure_message_for_should do |text| "expected '#{@content}' to contain '#{text}'" end failure_message_for_should_not do |text| "expected #{@content} to not contain '#{text}'" end end end end
Factory Girl gem change
If you’re using Factory Girl, replace factory_girl in your Gemfile with factory_girl_rails
I don’t know the rationale behind the change but cest la vie.
edit: 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.
Assert difference has disappeared
Another 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
Anyways, to get my assert_difference and assert_no_difference assertions working again, I created the file spec/support/assert_difference.rb:
def assert_difference(executable, how_many = 1, &block) before = eval(executable) yield after = eval(executable) after.should == before + how_many end def assert_no_difference(executable, &block) before = eval(executable) yield after = eval(executable) after.should == before end
Hey I’m getting closer, down to 72 failures.
Loading files in the lib directory
The ruby files in lib/ are no longer automatically loaded by Rails. So in application.rb I had to add this:
module Matchbook class Application < Rails::Application require Rails.root.join("lib", "matchbook", "delayed_jobs", "delete_contacts_job.rb") require Rails.root.join("lib", "matchbook", "delayed_jobs", "email_creator_job.rb") require Rails.root.join("lib", "matchbook", "auditing", "contact_audit_sweeper.rb") ... end end
More Rspec pain
I also get a few of these errors:
unexpected method invocation:
valid?(nil)
expected invocations:
- valid?()Come on, why aren’t they treated as equivalent now?
Well since there’s only a few of them I went and manually added the nil parameter.
Action Mailer changes
According to the docs ActionMailer methods always return objects, not strings:
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.
So where these tests used to pass
mail = SiteMailer.email(message, contact, {"message" => message_link, "image" => image_link, "promo" => promo_link}) mail.body.should include_text(message_link.to_s) mail.body.should include_text(image_link.to_s) mail.body.should include_text(promo_link.to_s)
they now fail. That’s easily fixed with:
mail = SiteMailer.email(message, contact, {"message" => message_link, "image" => image_link, "promo" => promo_link}) body = mail.body.encoded.gsub(/=\r\n/, "") body.should include_text(message_link.to_s) body.should include_text(image_link.to_s) body.should include_text(promo_link.to_s)
Note I updated the SiteMailer code to work with the new Rails mailer API. Info in the guide.
Ruby Double mocking with deprecations
This no longer works:
any_instance_of(Note, :validate => nil)
and also produces deprecation warnings. This isn’t the correct rr syntax anyway, so I replaced it with
stub.any_instance_of(Note).valid?(nil) { true }
Display the full stacktrace
Run specs with the -b option to include the full backtrace, rather than just the subset which includes your code.
This is useful for identifying problems in libraries and dependencies.
All tests passing
With 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!
You may also be interested in:
Comments //
Post a comment
Recent Tweets
Blog: The monumental Myspace cock-up: http://bit.ly/emgRKV
Tweeted on
Friday at 09:43
Awww railsapi, delete some logs: http://bit.ly/htBNDH
Tweeted on
Wednesday at 16:15
David Chelimsky Oct 20
I'll update the docs with things you've cited that are actually missing, but most of this information is actually readily available. Please read the following and update your post accordingly:
1. http://rspec.info - "More Information" (links to RSpec-2 documentation).
2. http://github.com/rspec/rspec-rails (README - linked from http://rspec.info).
3. http://github.com/rspec/rspec-rails/blob/master/Upgrade.markdown
Additional notes:
a. `assert_difference` is a Rails assertion. RSpec's `should change` is still available and at your disposal.
b. you're using RR for mocking. That's what is providing failure messages about invocations with or without nil.
Hope this helps you and your readers.
Cheers,
David
Kurt Snyder Feb 01
There's a cheat sheet of RSpec1 to RSpec2 changes here: http://snyderscribbles.blogspot.com/2011/01/rspec-2-changes-from-rspec-1.html I'll post additions/corrections that anyone wants to submit.
Kimlueng Sep 14
Thanks for the pointer. That'll help if I need to conrvet anything else. I'm impressed by how much Rspec has changed in just the last couple of months. I think the older release I was working from dates back only as far as the start of April.
chinacheng Feb 22
i think the assert_difference method could be more better, for example: executable maybe a array