Desde que trabajo con Ruby on Rails de manera intensiva cada día, una de las cosas que más me incordia es abandonar el cómodo entorno de script/console para lanzar la (al menos para mí) menos cómoda interfaz de mysql La razón por la que me veo obligado a hacer esto con cierta frecuencia es para encontrar los ids de los objetos ActiveRecord que me interesan para investigar o depurar alguna funcionalidad. Cuando la aplicación tiene cientos de registros en las tablas, viene muy bien poder hacer consultas por el nombre aproximado con la construcción LIKE de SQL. ActiveRecord nos permite escribir nuestro propio SQL pero como soy un vago me resulta bastante tedioso escribir la condición completa con la sintaxis que pide Rails, así que normalmente ando saliendo de la consola, escribiendo una sentencia SELECT ... LIKE en mysql y volviendo a la consola con el id apuntado.
Hasta hoy, que me he terminado de cansar y he escrito un plugin que permite hacer sentencias LIKE desde ActiveRecord de manera dinámica.
Finders dinámicos en ActiveRecord
Los finders dinámicos de ActiveRecord son una de las primeras perlas de Ruby con las que uno se encuentra cuando investiga las tripas de Rails. Supongo que serán conocidos, pero no está de más repasarlos un poco.
Supongamos que tenemos una clase ActiveRecord mapeada sobre una tabla MyTable con los atributos atributo1, atributo2 y atributo3, sabemos que podemos escribir:
Mytable.find_by_id(87) |
Y ActiveRecord tratará de encontrar el registro identificado con el id 87. No parece nada del otro mundo, porque entre otras convenciones ActiveRecord asume que la clave primaria en nuestras tablas será una columna llamada id, por lo que podría ser que existiese un método find_by_id en ActiveRecord::Base
Pero resulta algo más sorprendente toparse con que sentencias como:
Mytable.find_by_atributo1('Valor') |
también funcionan. Aquí comienza a saborearse el dinamismo de Ruby, y podemos asumir que ActiveRecord, de manera astuta, construye tantas funciones Mytable::find_by_... como atributos tengamos en la tabla, que lanzarán la correspondiente consulta SQL a la base de datos.
Pero.. ¡un momento!
1 2 |
Mytable.find_by_atributo1_and_atributo3('Valor', 58) Mytable.find_by_atributo1_and_atributo2_and_atributo3('Valor',58,'Valor3') |
¡También! funcionan. Y sería ridículo pensar que ActiveRecord construye funciones de manera dinámica para todas las combinaciones posibles de los atributos (y, además, en cualquier orden que queramos)
Nuestro propósito es añadir más finders dinámicos, que en lugar de búsquedas exactas hagan búsquedas aproximadas, invocándose de la siguiente manera:
Mytable.find_like_atributo1('al')
Para hacerlo, tenemos que entender bien qué hace ActiveRecord con estas misteriosas funciones dinámicas…
La magia de method_missing
A estas alturas ya nos imaginamos que no se añaden métodos para cada atributo, sino que hay algún otro mecanismo actuando en este caso. Se trata de method_missing que es el método que ejecuta una clase Ruby cuando se le invoca un método que no tiene implementado. Aquí podeis ver otro uso muy creativo de method_missing
ActiveRecord se aprovecha de method_missing para articular los finders dinámicos. Si abrís el fichero lib/active_record/base.rb vereis que la clase ActiveRecord::Base define un method_missing muy especial (si no estais viendo ese código, hacedlo ahora: leer código del core de Rails es siempre un ejercicio inspirador). Este método se activa cuando llamamos a Mytable.find_by_attributo('valor'), y lo primero que hace es comprobar con una expresión regular si el método invocado tenía la pinta find_by o find_all_by, en cuyo caso pasa a construir una sentencia SQL de búsqueda según los atributos y parámetros recibidos. Parece evidente que nosotros tendremos que puentear este método y hacer algo parecido por nuestra cuenta pero construyendo sentencias SQL con el modificador LIKE.
Cómo construir nuestro propio finder
Lo primero es preparar un plugin, lo cual es tan fácil como crear un directorio find_like en vendor/plugins, y ahí escribiremos un fichero init.rb con el siguiente contenido:
require 'find_like' |
A continuación, creamos el directorio vendor/plugins/find_like/lib y ahí pondremos el código de nuestro plugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
module ActiveRecord class Base class << self private def construct_conditions_from_like_arguments(attribute_names, arguments) conditions = [] attribute_names.each_with_index{ |name, idx| conditions <<= "#{table_name}.#{connection.quote_column_name(name)} LIKE '%%" << arguments[idx] << "%%'"} [conditions.join(" AND "), *arguments[0..attribute_names.length]] end alias_method :previous_method_missing, :method_missing def method_missing(method_id, *arguments) if match = /find_(all_like|like)_([_a-zA-Z]\w*)/.match(method_id.to_s) finder = (match.captures.first == 'all_like' ? :find_every : :find_initial) attribute_names = extract_attribute_names_from_match(match) super unless all_attributes_exists?(attribute_names) conditions = construct_conditions_from_like_arguments (attribute_names, arguments) options = {:conditions => conditions} set_readonly_option!(options) send(finder, options) else previous_method_missing(method_id, *arguments) end end end end end |
El código más o menos es fácil de seguir: usamos class << self para abrir la clase ActiveRecord::Base y modificar sus propias tripas. Dado que estamos volviendo a escribir el método method_missing no querremos perder la funcionalidad ofrecida por el method_missing originalmente incluido con ActiveRecord::Base (el que veíamos antes en base.rb) así que simplemente haremos un alias a este método y lo llamaremos desde nuestro method_missing si detectamos que el método invocado no es uno de los que nosotros queremos controlar (find_like o find_all_like)
Tras esto, podemos lanzar una consola y comprobar que, por arte de birlibirloque, ya tenemos nuestros finders operativos:
1 2 3 4 5 6 7 8 9 |
Tamarindo:~/src/repos/dconrails juan$ script/console development Loading development environment. >> Algoritmo.find_all_like_nombre("oyal") => [#<Algoritmo:0x245c908 @attributes={"status"=>"BEGIN", "nombre"=>"RoyalRoadDemo"}>, #<Algoritmo:0x245c82c @attributes={"status"=>"RUN", "nombre"=>"RoyalRoad2">] >> Algoritmo.find_all_like_nombre_and_status("oyal", "UN") => [#<Algoritmo:0x243dabc @attributes={"status"=>"RUN", "nombre"=>"RoyalRoad2"}>] |
Son conocidos los screencasts de Peepcode, obra del magno Topfunky. Diría que se tratan de un recurso imprescindible: por un puñado de dólares (algo menos de euros) uno recibe una densa hora de conceptos y consejos de alta calidad.
Pues bien, hoy he descubierto Railscasts, donde Ryan Bates publica screencasts breves con trucos y consejos de Rails. Uno puede, además, suscribirse en iTunes, lo que lo hace todo aún más fácil.
Y, sí, Peepcode también tiene algo que ver con esto.
Aunque yo vengo usando Textmate como entorno de desarrollo para Rails en el Mac, a veces echo de menos muchas cosas de Emacs, como por ejemplo las vistas divididas y los buffers con eshell.
Hasta ahora, si estabas en Linux tenías, básicamente, dos opciones como entorno de desarrollo con cierto soporte de Rails: o usas vim o bien usas Eclipse o RadRails. Emacs, en cierto modo, quedaba por detrás.
Por eso me ha alegrado encontrar este modo menor para Emacs que añade soporte para un montón de prestaciones Rails: controlar Mongrel o Webrick, ejecutar tareas de Rake o acceso a la documentación RDoc.
Con motivo de los inminentes cambios en el blog, la dirección del feed RSS de SobreRailes ha cambiado a:
http://feeds.feedburner.com/SobreRailes
Por favor, actualicen sus agregadores.
Del zeitgeist de la pasada Conferencia Rails me quedaría con la idea de que el testing de nuestra aplicación es, no ya posible, sino inexcusable. Así que llevo unos días dándole vueltas a las herramientas de testing que ofrece Rails, más allá de las conocidas técnicas de pruebas unitarias de las que ya hemos hablado por aquí alguna vez
Así que hoy trataré de indagar un poco qué posibilidades tenemos para probar las vistas, que es un territorio más pastoso y maleable que el mundo de los modelos, más
predecibles y modosos. Hasta ahora la gran herramienta disponible para verificar aserciones sobre el HTML generado por nuestra aplicación era assert_tag,
pero de la mano de Assaf Harkin, ha llegado un nuevo pistolero a la ciudad llamado
assert_select que de hecho ha acabado entrando en el core de Rails) (si bien también está disponible como plugin).
Pruebas del HTML para llamadas GET y POST estándar
Para introducir a este nuevo amiguito vamos a jugar con la clásica aplicación de libros y autores. Tendremos, pues, una base de datos con esta pinta:
1 2 3 4 5 6 7 8 9 10 11 |
ActiveRecord::Schema.define(:version => 1) do create_table "authors", :force => true do |t| t.column "name", :string end create_table "books", :force => true do |t| t.column "name", :string t.column "author_id", :integer end end |
No hay sorpresas en nuestros modelos:
1 2 3 4 5 6 7 |
class Author < ActiveRecord::Base has_many :books end class Book < ActiveRecord::Base belongs_to :author end |
Crearemos un andamiaje con script/generate scaffold Author y ya podemos empezar a jugar con la aplicación con script/server. Si entramos en el controlador authors, podemos dar de alta un par de autores y ver lo que nos devuelve la acción index:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<body> <h1> Listing authors </h1> <table> <tr><th>Name</th></tr> <tr> <td>Isaac Asimov</td> <td><a href="/authors/show/1">Show</a></td> <td><a href="/authors/edit/1">Edit</a></td> </tr> <tr> <td>Carl Sagan</td> <td><a href="/authors/show/2">Show</a></td> <td><a href="/authors/edit/2">Edit</a></td> </tr> (etcétera) </table> </body> |
¿Podemos escribir una prueba que nos asegure que la tabla se está construyendo de esta manera y no de otra? La respuesta es que sí. Para ello, necesitamos crear una
fixture para nuestros tests con los datos de la tabla de autores que vamos a considerar en nuestras pruebas, por ejemplo:
1 2 3 4 5 6 7 8 9 |
first:
id: 1
name: Carl Sagan
another:
id: 2
name: Isaac Asimov
last:
id: 3
name: Arthur Clarke |
Y ahora entra en acción assert_select. En nuestro archivo con las pruebas funcionales para el controlador de autores test/functional/authors_controller_test.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
def test_list get :list assert_response :success assert_template 'list' assert_select "table" do assert_select "tr>th", "Name" assert_select "tr:nth-child(2)>td", "Carl Sagan" assert_select "tr:nth-child(3)>td", "Isaac Asimov" assert_select "tr:nth-child(4)>td", "Arthur Clarke" end end |
Con el primer assert_select nos estamos quedando con el primer elemento table del HTML devuelto al invocar la acción list de nuestro controlador. En el bloque pasado como parámetro las siguientes assert_select actuarán sobre elementos HTML que cuelguen del tag table.
La sintaxis de de los selectores que escogen nodos del árbol de etiquetas de la página no es precisamente sencilla, pero cubre todos los casos posibles. Teneis un chuletario aquí
Selectores CSS
La mayor funcionalidad que nos da assert_select es que que tenemos a nuestra disposición todos los selectores CSS, con lo que quizá podríamos escribir nuestra prueba de
una manera más sencilla (y con un ejemplo más parecido al Mundo Real):
1 2 3 |
<% for column in Author.content_columns %> <td class="author_name"><%=h author.send(column.name) %></td> <% end %> |
Ahora podemos afinar la selección sin tener que buscar directamente el tag de la tabla.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def test_list_with_css get :list assert_response :success assert_template 'list' autores = ["Carl Sagan", "Isaac Asimov", "Arthur Clarke"] assert_select ".author_name" do autores.each do |name| assert_select "td", name end end end |
Rizando el rizo, también podríamos escribir esto tomando directamente todas las fixtures sin necesidad de tener en una lista los nombres duplicados:
1 2 3 4 5 6 7 8 9 10 11 12 |
def test_list_with_css_no_array get :list assert_response :success assert_template 'list' assert_select ".author_name" do Author.find(:all).each do |author| assert_select "td", author.name end end end |
Con assert_select podemos proteger nuestro código Rails de cambios indeseados (o inesperados) en la plantilla (si por error ponemos .autor_name en la plantilla list.rhtml nuestros tests empezarán a fallar indicándonos que algo va mal antes de que nos llame un cliente preguntando por qué con el último cambio en producción se han dejado de ver los nombres de los clientes en color azul-lapislázuli).
¿Y qué pasa con las llamadas Ajax?
No hay problema, select_tag nos permite también cubrirnos las espaldas. Supongamos que en nuestra vista tenemos ahora un enlace al método books que nos devuelve una lista de los libros para un autor dado, actualizando de manera asíncrona un div en nuestra plantilla:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<% for author in @authors %> <tr> <% for column in Author.content_columns %> <td class="author_name"><%=h author.send(column.name) %></td> <% end %> <td><%= link_to 'Show', :action => 'show', :id => author %></td> <td><%= link_to 'Edit', :action => 'edit', :id => author %></td> <td><%= link_to 'Destroy', { :action => 'destroy', :id => author }, :confirm => 'Are you sure?', :method => "post" %></td> <td><%= link_to_remote 'Show books', :url => {:action => 'books', :id => author} %></td> </tr> <% end %> <div libros> </div> |
La acción books del controlador AuthorController, es tan trivial como
1 2 3 |
def books @books = Author.find(params[:id]).books end |
Con la plantilla books.rjs
page.replace_html 'libros', :partial => 'book', :collection => @books |
Y el parcial _book.rhtml:
<p class="book"><%= book.name %></p> |
Creando el fichero de fixtures correspondientes e incluyéndolas con :fixtures 'books' en nuestro authors_controller_test (ojo con este paso que si se olvida para Rails sería como si no hubiésemos definido ningún fixture para nuestras pruebas), pasamos a escribir nuestra prueba:
1 2 3 4 5 6 7 8 9 10 11 |
def test_books xhr :get, :books, :id => 1 assert_select_rjs "libros" do assert_select ".book" do assert_select "p", "Cosmos" assert_select "p", "Contact" assert_select "p", "Broca's Brain" end end end |
Simplemente invocamos assert_select_rjs, indicándole el selector de la parte que se va actualizar con nuestra llamada Ajax (si no se actualizase el HTML del div con id libros, la primera aserción fallaría). Tras eso, podemos proceder que en el caso no-Ajax.
Aquí está el PDF de mi presentación en la Conferencia Rails 2006, donde presenté el proyecto DConRails, la criatura nacida de las mentes maestras de JJ Merelo y un servidor.
Para ahorrar tiempo al que no le interese, diré que se trata de presentar al navegador web (sí, ese programita con el que estás viendo ahora mismo esta bitácora) como lo que verdaderamente es: una poderosa herramienta de computacón distribuida, gracias a Rails, AJAX y tal y tal. Lógicamente, en la web no podreis ver la impresionante demo donde tratábamos de resolver un problema de computación tan formidable como rellenar una cadena de unos (y, dicho sea de paso, no acabamos)

