homeASCIIcasts

226: Upgrading to Rails 3 Part 2 

(view original Railscast)

Other translations: Pt It Es

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.

The site is now up and running.

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 contstant ApplicationHelper::Textilizer

In this application Texilizer 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.

/config/application.rb

# 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 show page is still throwing errors.

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.

/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.

/config/application.rb

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.

The page loads but looks wrong.

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.

/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
      create  .rspec
       exist  spec
    conflict  spec/spec_helper.rb
Overwrite /Users/eifion/rails/apps_for_asciicasts/ep226/railscasts/spec/spec_helper.rb? (enter "h" for help) [Ynaqdh] Y
       force  spec/spec_helper.rb
      create  autotest
      create  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:

/spec/spec/helper.rb

# 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
152 examples, 4 failures
1) CommentsController as guest index action should render index template for rss with xml
    Failure/Error: response.should have_tag('title', :text => 'Railscasts Comments')
    undefined method `has_tag?' for #<ActionController::TestResponse:0x000001040f2dc8>
    # ./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')
    undefined method `has_tag?' for #<ActionController::TestResponse:0x00000104007710>
    # ./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/)
    undefined method `has_tag?' for #<ActionController::TestResponse:0x00000102e2dd00>
    # ./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/)
    undefined method `has_tag?' for #<ActionController::TestResponse:0x000001041f03b0>
    # ./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
152 examples, 0 failures

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.

Soon-to-be-deprecated ActiveRecord calls
Methods such as find(:all), find(:first), finds with conditions, and the :joins option will soon be deprecated.
More information: http://m.onkey.org/2010/1/22/active-record-query-interface

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.

/app/models/episode.rb

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.

/app/models/episode.rb

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:

named_scope is now just scope
The named_scope method has been renamed to just scope.
More information: http://github.com/rails/rails/commit/d60bb0a9e4be2ac0a9de9a69041a4ddc2e0cc914

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

/app/models/comment.rb

named_scope :recent, :order => "created_at DESC"

will become

/app/models/comment.rb

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:

/config/routes.rb

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.

/config/routes.rb

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.

Deprecated ERb helper calls
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: http://weblog.rubyonrails.org/

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.

/app/views/episodes/archive.html.erb

<% 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.

/app/views/episodes/archive.html.erb

<%= 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.

/app/views/episodes/archive.html.erb

<% @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.