#226 Upgrading to Rails 3 Part 2
En el episodio anterior vimos cómo actualizar una aplicación de Rails 2 a Rails 3, utilizando el sitio web de Railscasts como ejemplo. Llegamos hasta el punto de que la aplicación arrancaba sin dar errores. Sin embargo, hoy repasaremos algunas cosas que aún nos quedaban por corregir o actualizar.
Resulta obvio que una forma de ver qué nos queda por corregir ese ejecutar las suites de tests y especificaciones, pero antes de eso conviene dedicar algunos minutos a repasar la aplicación con el navegador para ver si podemos detectar errores obvios. Al hacer esto con la aplicación de Railscasts hemos descubierto que si bien la página de índice de episodios funciona correctamente, la página de un episodio concreto devuelve el siguiente error:
uninitialized constant ApplicationHelper::Textilizer
Textilizer
en esta aplicación es una clase escrita por nosotros que reside en el directorio /lib
, lo que es causa de problemas porque los archivos del directorio lib
ya no se añaden a la ruta de inclusión por defecto en Rails 3. Para corregir el problema, podemos o bien añadir el directorio a la ruta o bien hacer require
manualmente de los archivos del directorio /lib
.
Vamos a escoger la primera opción y añadiremos el directorio a autoload_paths
. En el fichero /config/application.rb
hay una línea comentada que tendremos que modificar.
# Custom directories with classes and modules you want to be autoloadable. config.autoload_paths += %W(#{Rails.root}/lib)
El equipo de Rails aún está trabajando en la mejor manera de gestionar la carga de archivos del directorio /lib
, por lo que puede que en la versión final de Rails esto cambie.
Cuando recargamos la página nos topamos con un error diferente.
La aplicación se queja de que no existe la constante APP_CONFIG
. Esta constante está definida en un fichero de preinicialización llamado /config/preinitializer.rb
.
# load app_config.yml require 'yaml' APP_CONFIG = YAML.load(File.read("#{Rails.root}/config/app_config.yml"))
Este es el sitio donde, en una aplicación Rails 2, pondríamos las cosas que queremos definir antes de que cargue la aplicación propiamente dicha. En este caso se trata de cargar un archivo YAML. Pero este funcionamiento ya no está soportado en Rails 3 por lo que cualquier código que tengamos en preinitializer.rb
debe moverse al fichero /config/application.rb
. Dicho código debe aparecer lo más cerca posible del comienzo del fichero, justo antes de la línea require 'rails/all'
.
Tampoco podemos pegar el código tal cual porque utiliza Rails.root
, que no estará disponible hasta que ejecutemos rails/all
por lo que tendremos que hacer referencia al archivo de forma relativa al fichero 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'
Si volvemos a cargar la página veremos que carga pero no tiene muy buen aspecto. El HTML de la sección de notas está siendo escapado y falta la barra lateral derecha. Pero, básicamente, la página funciona por lo que nos damos por satisfechos en esta primera inspección de la aplicación en la que sólo estamos viendo cada página para ver si se lanzan excepciones. Una vez que hayamos corregido las páginas que devuelvan errores podemos dar una segunda pasada y corregir este tipo de errores.
Ejecución de los tests de aplicación
Si ejecutamos otra vez rake rails:upgrade:check
veremos que sigue apareciendo una lista bastante extensa de cosas que corregir. La mayoría de estos problemas no son errores propiamente dichos sino que son avisos de métodos que son a extinguir (funcionarán todavía con Rails 3.0 pero probablemente no lo hagan tras Rails 3.1) Dado que los cambios que hemos de hacer son para este tipo de avisos le echaremos un vistazo a la suite de pruebas de la aplicación y nos aseguraremos de que todos los tests pasan antes de hacer ningún cambio sobre el código.
Para que los tests pasen tenemos que añadir las gemas pertinentes a nuestro Gemfile
. Queremos que estas gemas sean sólo sean cargadas en su entorno correspondiente por lo que utilizaremos el método group
. Podría parecer que sólo querríamos ejecutar estas gemas para el entorno de tests pero también tendremos que incluir gemas que incluyen tareas de Rake (como RSpec) en el entorno de desarrollo.
La aplicación de Railscasts usa Mocha como librería de mocking, así como RSpec y Factory Girl por lo que añadiremos el siguiente código al fichero Gemfile
.
group :development, :test do gem "mocha" gem "rspec-rails", ">= 2.0.0.beta.19" gem "factory_girl_rails" end
Una vez más ejecutamos bundle install
para asegurarnos de que tenemos todas estas gemas instaladas y después para poner en marcha RSpec tendremos que ejecutar su generador. El generador sobreescribirá los ficheros RSpec que ya estén instalados por lo que tenemos que hacer copia de seguridad:
$ 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
Tendremos que hacer un pequeño cambio en el fichero spec_helper.rb
que ha sido regenerado antes de poder continuar. Por defecto usará el mocking de Rspec así que cambiaremos la línea config.mock_with
para que utilice Mocha.
También habrá que incluir algunas macros de RSpec personalizadas que existen en /spec/controller_macros.rb
. Un poco más abajo en el fichero spec_helper
hay una línea de código que incluye todo lo que exista en el directorio support/
, así que lo único que tenemos que hacer es crear este directorio colgando del directorio spec/
y mover el archivo ahí. Para incluir las macros en RSpec tenemos que añadir una línea de tipo config.include
para que se incluya nuestro módulo particular. El fichero quedará así después de estos cambios:
# 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
Ya estamos listos para ejecutar las especificaciones para ver cuántas pasan. Sabemos que habrá un montón de avisos de funcionalidades que en breve dejarán de funcionar pero ahora nos centraremos en detectar qué tests han dejado de pasar.
$ rake spec
Este comando produce una gran cantidad de información, y por último indica que la ejecución de tests devolvió un error. La línea en concreto es:
/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)
Si vemos mensajes de este tipo y no estamos seguros acerca de cómo corregirlos, conviene buscar en Google el error para ver si alguien se ha encontrando con él antes y ha dado con una solución. En este caso el arreglo es simple, al método integrate_views
se le ha rebautizado como render_views
, por lo que tenemos que cambiar esa cadena en todos los lugares en los que se esté usando.
Cuando lo hayamos hecho podemos volver a ejecutar los tests. Otra vez aparecerá la ristra de advertencias pero de entre toda esa salida veremos que pasan todos los tests menos cuatro.
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)>'
La causa de todos estos errores es la misma: el método have_tag
. Si buscamos en Internet veremos rápidamente que tiene que ver con Webrat, donde se ha eliminado el método have_tag
. En su lugar podemos usar el método have_selector
. Las opciones de have_selector
son ligeramente diferentes: tan sólo tenemos que cambiar la opción :text
por :content
, de forma que por ejemplo:
response.should have_tag('title', :text => 'Railscasts')
se convierte en
response.should have_selector('title', :content => 'Railscasts')
Veremos que todos los tests pasan una vez que hayamos hecho este cambio.
Finished in 3.44 seconds <span class="passed">152 examples, 0 failures</span>
Eliminación del código a extinguir
Una vez que nos pasan todos los tests podemos empezar con la lista que nos generó rake rails:upgrade:check
para reducir el número de mensajes que nos avisan de funcionalidades que van a desaparecer. El primer elemento de esta lista tiene que ver con las llamadas a ActiveRecord.
<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>
Esto se refiere a las líneas de código que utilizan la antigua sintaxis find
, tales como este método del modelo Episode que recibe un hash con condiciones.
def self.primitive_search(query) find(:all, :conditions => primitive_search_conditions(query)) end
Podemos actualizar este código usando el nuevo método where
.
def self.primitive_search(query) where(primitive_search_conditions(query)) end
Cubrimos la nueva sintaxis de consultas en ActiveRecord en el episodio 202 [verlos, leerlo], donde se pueden encontrar más detalles al respecto.
El siguiente error de la lista es:
<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>
Se trata de otro problema fácil de corregir. Sólo tenemos que recorrer los modelos y cambiar las llamadas a named_scope
y cambiarlas por scope
. Por supuesto también tendremos que ajustar los argumentos a la nueva sintaxis de Rails 3. Así, por ejemplo, en el modelo Comment
:
named_scope :recent, :order => "created_at DESC"
la llamada pasa a ser
scope :recent, order("created_at DESC")
Conviene tener en cuenta que según vamos haciendo estos cambios en la aplicación deberíamos correr de nuevo la suite de tests para estar seguros de no haber roto nada.
El siguiente elemento tras los ámbitos nominales son las rutas. El plugin de actualización de Rails incluye una tarea de Rake para actualizar el fichero de rutas pero es mejor dar el repaso nosotros mismos y aprovechar la ocasión para reorganizarlo un poco.
Este es el aspecto actual del fichero de rutas:
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
No veremos aquí la nueva sintaxis de las rutas porque ya las estudiamos en el episodio 203 [verlo, leerlo]. Tras los cambios pertinentes, el aspecto del fichero de rutas es el siguiente:
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
El nuevo fichero de rutas es un poco más largo pero desde luego está más ordenado.
Lo último que nos queda pendiente son las llamadas a helpers de ERb a extinguir.
<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>
En el código de vistas de aplicaciones en Rails 3 a veces es necesario utilizar <%=
en
lugar de <%
al comienzo de los bloques que devuelven contenido tales como form_for
.
En el fichero archive.html.erb
podemos ver un ejemplo de esto:
<% form_tag archive_episodes_path, :method => 'get' do %> <p> <%= text_field_tag :search, params[:search] %> <%= submit_tag "Search", :name => nil %> </p> <% end %>
La etiqueta form_tag
de este código va a rodear de más código el marcado devuelto por el bloque por lo que tenemos que cambiarla para que utilice el signo de igualdad.
<%= form_tag archive_episodes_path, :method => 'get' do %>
Sin embargo no hay que cambiar todos los bloques en el código de las vistas aunque rake rails:upgrade:check
nos lo diga. Por ejemplo en el código de abajo cuando iteramos después de cada elemento del hash no tenemos que poner el símbolo de igualdad porque el código no añade ninguna etiqueta al marcado HTML devuelto por el bloque.
<% @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 %>
En el episodio 208 [verlo, leerlo] se examinan con detalle los motivos de este cambio.
Aunque hayamos terminado corrigiendo todo el código de las vistas el chequeo de rails:upgrade:check
seguirá fallando, pero serán falsas alertas porque nos indicará que hay que corregir los bloques en todas las vistas. Recordemos que sólo debíamos cambiar las que añaden contenido HTML a lo devuelto por su bloque, tales como form_for
, form_tag
, div_for
, etc. Si no estamos seguros de si debemos o no añadir el símbolo de igualdad es mejor no ponerlo y buscar posteriormente los mensajes de aviso en los registros de la aplicación.
Una vez que hayamos concluido con el uso del plugin de actualización podemos desinstalarlo ejecutando
$ rails plugin remove rails_upgrade
Hemos hecho un gran avance en nuestra actualización a Rails 3. Ya pasan todos los tests y hemos eliminado todo el código que va a dejar de estar soportado en versiones futuras. Seguimos teniendo los problemas que vimos anteriormente, donde parte del HTML de la página estaba escapado y faltaba la barra lateral. Veremos cómo corregirlo en el próximo episodio.