#226 Upgrading to Rails 3 Part 2
In the last episode we showed you how to update a Rails 2 application to Rails 3, using the Railscasts site as an example. We got as far as getting the application to boot without throwing any errors but there are still things that need fixing or updating and we’ll be covering those here.
One obvious way to see what remains to be fixed is to run the application’s tests or specs, but before we look at those it’s worth taking a few minutes to run through the application in a browser so that we can spot any obvious errors. When we do this for the Railscast app we’ll quickly discover that while the episodes index page now works the page for a single episode doesn’t and throws the following error.
uninitialized constant ApplicationHelper::Textilizer
In this application Textilizer
is a custom class in the /lib
directory and that is causing a problem as by default in the first Rails 3 release candidate files in the /lib
directory are no longer automatically included in the application’s load path. To fix this error we can either manually require
the files in the /lib
directory or add the directory to the path.
We’ll take the second option and add the directory to the autoload_paths
. There is a commented-out line in the /config/application.rb
file that we can alter to do this.
# Custom directories with classes and modules you want to be autoloadable. config.autoload_paths += %W(#{Rails.root}/lib)
The core Rails team is still working out the best way to handle loading files from the /lib
directory so this may change by the final Release of Rails 3.
When we reload the page now we’ll get a different error.
The page now throws an error related to a missing APP_CONFIG
constant. This constant is defined in a pre-initializer in /config/preinitializer.rb
.
# load app_config.yml require 'yaml' APP_CONFIG = YAML.load(File.read("#{Rails.root}/config/app_config.yml"))
In a Rails 2 application this is where we’d put things that we want to define before the application loads and in this case it is used to load a custom YAML file. The preinitializer.rb
file isn’t supported in Rails 3 so any code in there should be moved into the /config/application.rb file
. Code from the preinitializer file needs to go near the top of the file, just before the line require 'rails/all'
.
We can’t just paste the code in as it stands as it uses Rails.root
, but this isn’t available before we require rails/all
. Instead we’ll have to reference the file relative to the application.rb
file.
require File.expand_path('../boot', __FILE__) # load app_config.yml require 'yaml' APP_CONFIG = YAML.load(File.read(File.expand_path('../app_config.yml', __FILE__))) require 'rails/all'
When we reload the page again it loads up but it doesn’t look quite right. The HTML in the notes section is being escaped and the righthand sidebar is missing. But the page basically loads and this is good enough for a first sweep through the application as all we’re doing is going through each page to see if any exceptions are raised. Once we’ve fixed the pages that throw errors we can do a second sweep through each page and fix errors in the views like the one below.
Running The Application’s Tests
If we run rake rails:upgrade:check
again we’ll see that there’s still quite a list of issues to fix. These are mostly for things that have been deprecated rather than changed so they will still work under Rails 3.0 but are unlikely to continue working under Rails 3.1 As the changes we need to make are for deprecations rather than errors we’ll take a look at the application’s test suite next and make sure that the tests all pass before we make any changes to the code.
In order to get the tests running we’ll need to add the relevant gems to our Gemfile
. We only want these gems to be loaded for the relevant environments so we’ll use the group method to do this. It might seem that we’d just want to install the gems for the test environment but the gems that include Rake tasks such as RSpec will also need to be included in the development environment.
The Railscasts application uses Mocha for mocking, along with RSpec and Factory Girl so we’ll need to add the following code to the Gemfile
.
group :development, :test do gem "mocha" gem "rspec-rails", ">= 2.0.0.beta.19" gem "factory_girl_rails" end
To make sure that these gems are installed we’ll run bundle install
again and then to get RSpec up and running we’ll need to run its generator. The generator will overwrite the installed RSpec files so it’s worth backing them before before running it.
$ rails g rspec:install <span class="passed">create</span> .rspec <span class="info">exist</span> spec <span class="failed">conflict</span> spec/spec_helper.rb Overwrite /Users/eifion/rails/apps_for_asciicasts/ep226/railscasts/spec/spec_helper.rb? (enter "h" for help) [Ynaqdh] Y <span class="forced">force</span> spec/spec_helper.rb <span class="passed">create</span> autotest <span class="passed">create</span> autotest/discover.rb
The regenerated spec_helper.rb
file will need to have a small change made to it before we continue. By default it will use Rspec for mocking so we’ll need to update the config.mock_with
line so that it uses Mocha instead.
The application also has some custom RSpec macros in the /spec/controller_macros.rb
file and these will need to be included. Further up in the spec_helper
file is a line of code that includes everything under a support
directory so all we need to do is create this directory under the spec
directory and move the file into there. Then to include the macros into RSpec we need to add a config.include
line so that our custom module is included. After making these changes the file will look like this:
# This file is copied to ~/spec when you run 'ruby script/generate rspec' # from the project root directory. ENV["RAILS_ENV"] ||= 'test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' # Requires supporting files with custom matchers and macros, etc, # in ./support/ and its subdirectories. Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f} RSpec.configure do |config| config.mock_with :mocha config.fixture_path = "#{::Rails.root}/spec/fixtures" config.use_transactional_fixtures = true config.include ControllerMacros end
We’re ready now to run the specs and see how many of them pass. We know that there will be a large number of deprecation warnings but for now we’re just looking for failing tests.
$ rake spec
This command produces a large amount of output and then shows that the test run threw an error. The relevant line is:
/Users/eifion/rails/apps_for_asciicasts/ep226/railscasts/spec/controllers/episodes_controller_spec.rb:5:in `block in <top (required)>': undefined local variable or method `integrate_views' for :Class (NameError)
If you see error messages like this and aren’t sure how to fix them it’s worth Googling the error to see if anyone else has had the same problem and come up with a solution. In this case the fix is simple. The integrate_views
method has been renamed to render_views
and so we need to change that wherever it’s used in the application.
Once that’s done we can run the specs again. Again we’ll see a large number of deprecation errors in the output but among all those we’ll see that all but four of the tests pass.
Finished in 3.53 seconds <span class="failed">152 examples, 4 failures</span> 1) CommentsController as guest index action should render index template for rss with xml Failure/Error: response.should have_tag('title', :text => 'Railscasts Comments') <span class="failed">undefined method `has_tag?' for #<ActionController::TestResponse:0x000001040f2dc8></span> # ./spec/controllers/comments_controller_spec.rb:16:in `block (2 levels) in <main>' 2) EpisodesController as guest index action should render index template for rss with xml Failure/Error: response.should have_tag('title', :text => 'Railscasts') <span class="failed">undefined method `has_tag?' for #<ActionController::TestResponse:0x00000104007710></span> # ./spec/controllers/episodes_controller_spec.rb:26:in `block (2 levels) in <top (required)>' 3) EpisodesController as guest index action should render index template for rss with xml for iPod Failure/Error: response.should have_tag('title', :text => /Railscasts.+iPod/) <span class="failed">undefined method `has_tag?' for #<ActionController::TestResponse:0x00000102e2dd00></span> # ./spec/controllers/episodes_controller_spec.rb:33:in `block (2 levels) in <top (required)>' 4) EpisodesController as guest show action should render show template for rss with xml Failure/Error: response.should have_tag('title', :text => /Comments/) <span class="failed">undefined method `has_tag?' for #<ActionController::TestResponse:0x000001041f03b0></span> # ./spec/controllers/episodes_controller_spec.rb:65:in `block (2 levels) in <top (required)>'
All of the failures are caused by the same thing: the have_tag
method. Again a quick Internet search will help here and it turns out that this is related to Webrat and that have_tag
has been removed. There is now a similar have_selector
method that we can use instead. The options for have_selector
are slightly different, we’ll need to replace the :text
option with a :content
option so for example
response.should have_tag('title', :text => 'Railscasts')
will become
response.should have_selector('title', :content => 'Railscasts')
When we’ve made these changes we’ll run the tests again and this time they all pass.
Finished in 3.44 seconds <span class="passed">152 examples, 0 failures</span>
Removing Deprecated Code
Now that the tests all pass we can start working through the list that rake rails:upgrade:check
generates and reduce the number of deprecation warnings. The first item in the list regards ActiveRecord calls.
<span class="failed">Soon-to-be-deprecated ActiveRecord calls</span> Methods such as find(:all), find(:first), finds with conditions, and the :joins option will soon be deprecated. More information: <span class="url">http://m.onkey.org/2010/1/22/active-record-query-interface</span>
This refers to parts of the code that use the old find
syntax, such as this method in the Episode model that takes a hash of conditions.
def self.primitive_search(query) find(:all, :conditions => primitive_search_conditions(query)) end
We can update code like this by using the new where
method.
def self.primitive_search(query) where(primitive_search_conditions(query)) end
The new ActiveRecord query syntax was covered in episode 202 [watch, read] so for more details about this take a look there.
The next error in the list is this:
<span class="failed">named_scope is now just scope</span> The named_scope method has been renamed to just scope. More information: <span class="url">http://github.com/rails/rails/commit/d60bb0a9e4be2ac0a9de9a69041a4ddc2e0cc914</span>
This is another easy issue to fix. We just need to go through our models and replace any named_scope
calls with scope
. The arguments will also need to be updated to use the new Rails 3 syntax. So, for example, in the Comment
model
named_scope :recent, :order => "created_at DESC"
will become
scope :recent, order("created_at DESC")
Note that as we go through the application and make these changes we should keep rerunning our test suite to make sure nothing has been broken by them.
The next item after the named scopes is the routing. The rails upgrade plugin includes a rake task for upgrading a routes file but it’s better to go through it manually and take the opportunity to clean it up.
The routes file currently looks like this:
Railscasts::Application.routes.draw do |map| map.resources :spam_questions map.resources :spam_checks map.with_options :controller => 'info' do |info| info.about 'about', :action => 'about' info.contest 'contest', :action => 'contest' info.feeds 'feeds', :action => 'feeds' info.give_back 'give_back', :action => 'give_back' end map.login 'login', :controller => 'sessions', :action => 'new' map.logout 'logout', :controller => 'sessions', :action => 'destroy' map.resources :sponsors map.resources :comments map.resources :tags map.resources :episodes, :collection => { :archive => :get } map.resources :sessions map.resources :spam_reports, :member => { :confirm => :post }, :collection => { :confirm => :post } map.root :episodes end
The new routing syntax was covered in detail in episode 203 [watch, read] so we won’t go over it here. After the changes the routes file will look like this.
Railscasts::Application.routes.draw do root :to => "episodes#index" match "about" => "info#about", :as => "about" match "contest" => "info#contest", :as => "contest" match "feeds" => "info#feeds", :as => "feeds" match "give_back" => "info#give_back", :as => "give_back" match "login" => "sessions#new", :as => "login" match "logout" => "sessions#destroy", :as => "logout" resources :sponsors resources :comments resources :tags resources :episodes do collection do get :archive end end resources :sessions resources :spam_questions resources :spam_checks resources :spam_reports do member do post :confirm end collection do post :confirm end end end
The new routes file is a little longer but does look quite a bit cleaner.
The final item is deprecated ERb helper calls.
<span class="failed">Deprecated ERb helper calls</span> Block helpers that use concat (e.g., form_for) should use <%= instead of <%. The current form will continue to work for now, but you will get deprecation warnings since this form will go away in the future. More information: <span class="url">http://weblog.rubyonrails.org/</span>
In the view code in Rails 3 applications it’s sometimes necessary to use <%=
instead of <%
at the beginning of blocks that output content, such as form_for
.
An example of this is the code in the archive.html.erb file.
<% form_tag archive_episodes_path, :method => 'get' do %> <p> <%= text_field_tag :search, params[:search] %> <%= submit_tag "Search", :name => nil %> </p> <% end %>
The form_tag
in this code is going to insert content around what is inside its block so we do need to modify this to use an equals sign.
<%= form_tag archive_episodes_path, :method => 'get' do %>
Not every block in the view code needs to be changed, however, even though rake rails:upgrade:check
tells you to do so. For example in the code below where we loop through each item in a hash we don’t want to add an equals sign as the code doesn’t add any tags around the code in the block.
<% @episode_months.each do |month, episodes| %> <h2><%=h month.strftime('%B %Y') %></h2> <% for episode in episodes %> <div> <%= episode.position %>. <%= link_to episode.name, episode %> </div> <% end %> <% end %>
More information about this is available in episode 208 [watch, read].
Even after we’ve finished fixing all of the view code the upgrade check will still show errors and these are false as it is reporting all blocks in all views. Remember that you only want to change the ones that output content, such as form_for
, form_tag
, div_for
and so on. If you’re unsure as to whether a block should be changed to use the equals sign leave it off and then check for deprecation warnings in your application’s tests or development log.
Now that we’ve finished using the upgrade plugin we can uninstall it by running
$ rails plugin remove rails_upgrade
We’ve made a lot of progress in upgrading the application to Rails 3. The tests now all pass and we have removed all of the deprecated code. We still have the problems we saw earlier, however, where part of the HTML for the page was escaped and the sidebar was missing. We’ll cover all of this in the next episode.