Usando jQuery com Rails

04/06/08

Pode me chamar de velha guarda, mas prefiro escrever meu próprio Javascript. E depois que comecei a trabalhar com Ruby on Rails e jQuery, minha vida ficou infinitamente mais simples.

E dessa simplicidade, surgiu o rails.js, um dispatcher de eventos feito para ser usado com jQuery, no Rails.

A idéia é que, baseado no controller e action que está sendo renderizado, seja chamado o método correto do Javascript para aquele contexto.

Para ver como funciona na prática, crie um novo projeto com o comando abaixo.

rails pages

Você precisará de um controller chamado "pages", com algumas actions.

script/generate controller index show new create edit update

Crie também um arquivo de layout em "app/views/layouts/application.html.erb" com o conteúdo abaixo.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
    <head>
        <meta http-equiv="Content-type" content="text/html; charset=utf-8">
        <title>Pages</title>
        <%= javascript_include_tag "jquery", "rails", "pages" %>
 
        <meta name="rails-controller" content="<%= controller.controller_name %>" />
        <meta name="rails-action" content="<%= controller.action_name %>" />
    </head>
<body>
    <%= yield %>
</body>
</html>

O segredo está nesses dois campos meta, com os nomes do controller e action. É baseado neles que o rails.js irá chamar o método correto.

Agora vem o passo mais importante de todos: remova os arquivos do Prototype. Afinal, eles não servirão para nada! ;)

rm public/javascripts/*

Você precisará do jQuery e do rails.js. Copie-os para o diretório "public/javascripts". Crie também um arquivo em "public/javascripts/pages.js".

Agora, vem a parte legal! Por exemplo, imagine que você queira executar algum Javascript para as actions "index" e "new". Você pode criar algo como isso.

Rails.pages = {
    'index': function() {
        alert('calling js for index');
    },
 
    'new': function() {
        alert('calling js for new');
    }
}

Se você acessar o endereço http://localhost:3000/pages/, o método Rails.pages.index() será chamado.

E o que acontece se eu acessar a action "create", que normalmente é o fallback do método "new"? Quando você acessar a action "create", o método "new" será chamado no lugar, assim como a action "update" chamará o método "edit". Para ver como isso funciona, experimente acessar os endereços http://localhost:3000/pages/new e http://localhost:3000/pages/create.

Eu tenho uma opinião bastante forte a respeito do Rails gerar Javascript. É conveniente? Claro que é! Mas ainda sim prefiro escrever meu próprio Javascript.

Você pode fazer o download deste exemplo aqui.

Usando o RSpec para testar sua aplicação Rails - Modelos

01/06/08

Há muito tempo atrás, escrevi um artigo mostrando como testar uma aplicação Rails usando Test::Unit. Muita coisa aconteceu desde então e eu, influenciado pelo Arthur, comecei a usar o RSpec.

O RSpec é um framework BDD (Behaviour-Driven Development ou Desenvolvimento Guiado por Comportamento) escrito em Ruby, que permite que você escreva testes em uma linguagem mais natural, em inglês. O grande problema é que não fará muito sentido se você escrever tudo em português, mas se sua aplicação já é assim, então você deve estar acostumado com o código bilíngue.

Além do RSpec, existe um outro framework muito utilizado chamado Shoulda. Se você se sente confortável com o Test::Unit, vale a pena dar uma olhada.

Criando sua aplicação Rails

Crie uma aplicação chamada "todo" com o comando rails todo. Iremos criar uma lista de tarefas que podem ser marcadas como completadas. Ela será bastante simples, mas nos dará uma idéia de como testar modelos, controllers, helpers e views.

Instalando o RSpec

Para testar códigos Ruby que não fazem parte de uma aplicação Rails, você pode instalar a gem com o comando abaixo.

sudo gem install rspec

Agora, se você quer testar sua aplicação Rails — que é o nosso caso —, você deve seguir um outro caminho. À partir da versão 1.1.4, o RSpec utiliza o Github e para instalá-lo basta clonar seu repositório. Os comandos abaixo foram retirados do wiki do projeto.

cd vendor/plugins
git clone git://github.com/dchelimsky/rspec.git
cd rspec
git checkout 1.1.4
cd ..
git clone git://github.com/dchelimsky/rspec-rails.git
cd rspec-rails
git checkout 1.1.4
cd ../../../

Quando você terminar, execute o comando script/generate rspec para criar os arquivos necessários para o Rails utilizar o RSpec. Esse comando irá criar um diretório "spec", onde ficam localizados todos os arquivos de teste.

Entendendo o problema

Após pensar bastante sobre como nossa aplicação iria funcionar, chegamos aos seguintes requisitos:

  • Uma lista pode ter diversas tarefas
  • Uma lista deve ter um título de até 100 caracteres
  • Uma lista pode ter uma descrição opcional

Com isso em mente, podemos criar nosso modelo List, e escrever os testes antes de adicionar qualquer funcionalidade ao modelo. No RSpec, você possui alguns generators específicos para criar modelos e controllers, por exemplo. Para criar um modelo, você deve usar o generator "rspec_model".

script/generate rspec_model List title:string description:string
rake db:migrate

Ao gerarmos um modelo, o RSpec cria um arquivo de teste em diretório "spec/models/list_spec.rb". Vale lembrar que é possível se testar, separadamente, modelos, controllers, helpers e views, deixando tudo muito mais organizado.

Abra o arquivo arquivo "list_spec.rb", que já possui um teste — melhor ainda, uma especificação — criado automaticamente. No RSpec, uma especificação pode ser escrita usando o método it. Esse método deve estar associado a um outro método chamado describe, que terá diversas especificações para validar um determinado contexto.

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
 
describe List do
  before(:each) do
    @list = List.new
  end
 
  it "should be valid" do
    @list.should be_valid
  end
end

Vamos substituir este código por especificações reais que validem nosso modelo, já pensando em como evitar qualquer duplicação de código.

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
 
describe List do
  fixtures :lists
 
  it "should be valid" do
    lambda {
      list = create_list
      list.should be_valid
      violated "#{list.errors.full_messages.to_sentence}" if list.new_record?
    }.should change(List, :count).by(1)
  end
 
  it "should be invalid without a title" do
    lambda {
      list = create_list(:title => nil)
      list.errors.should be_invalid(:title)
    }.should_not change(List, :count)
  end
 
  it "should be invalid with long titles" do
    lambda {
      list = create_list(:title => "a" * (List::TITLE_MAX_LENGTH + 1))
      list.errors.should be_invalid(:title)
    }.should_not change(List, :count)
  end
 
  it "should accept description when provided available" do
    lambda {
      list = create_list(:description => "Things I need to buy at the supermarket")
      list.description.should_not be_nil
      list.should be_valid
    }.should change(List, :count)
  end
 
  it "should have many tasks" do
    lambda { lists(:work).tasks }.should_not raise_error
  end
 
  private
    def create_list(options={})
      List.create({
        :title => 'To buy'
      }.merge(options))
    end
end
 
 
Dica
Todos os métodos terminados em "?" (invalid?, completed?, valid?), podem ser chamados com o matcher be_<nome do método>, como be_invalid, be_completed e be_valid. Se o método espera qualquer argumentos, basta especificá-los, como em be_invalid(:title).

Nossas especificações utilizam fixtures. Por isso, vamos adicionar alguns itens ao arquivo "spec/fixtures/lists.yml".

supermarket:
  title: Supermarket
  description: Things I need to buy
 
work:
  title: Work
  description: Things to be done until Friday

Para rodar os testes, execute o comando "rake spec". Ele irá executar cada um dos arquivos de especificações presentes no diretório "spec".

exception:todo fnando$ rake spec
(in /Users/fnando/Sites/todo)
......F.FF.

1)
'List should have many tasks' FAILED
expected no Exception, got #<NoMethodError: undefined method `tasks' for #<List:0x230c6ac>>
./spec/models/list_spec.rb:37:

2)
NameError in 'List should be invalid with long titles'
uninitialized constant List::TITLE_MAX_LENGTH
/Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:478:in `const_missing'
./spec/models/list_spec.rb:23:
./spec/models/list_spec.rb:22:

3)
'List should be invalid without a title' FAILED
expected invalid?(:title) to return true, got false
./spec/models/list_spec.rb:17:
./spec/models/list_spec.rb:15:

Finished in 0.244484 seconds

5 examples, 3 failures

Os usuários do Textmate devem instalar o bundle RSpec; além de possuir os snippets, possui um comando que roda as especificações de maneira muito mais simples. Para instalá-lo, execute os comandos abaixo.

mkdir -p ~/Library/Application\ Support/TextMate/Bundles/
cd ~/Library/Application\ Support/TextMate/Bundles/
svn co svn://rubyforge.org/var/svn/rspec/trunk/RSpec.tmbundle

Depois de instalado, você pode executar todas as especificações com o atalho ⌘+R. Se preferir, pode executar uma única especificação com o atalho ⌘+⇧+R. Ambos os atalhos irão abrir uma janela como esta.

Janela do Textmate rodando os testes com RSpec

Para corrigir as falhas que nossas especificações geraram, vamos adicionar algum código em nosso modelo. A falha #3 é a mais simples de ser corrigida; basta adicionarmos uma chamada ao método validates_presence_of.

class List < ActiveRecord::Base
  validates_presence_of :title
end
 

A falha #2 espera utiliza uma constante que não existe; vamos adicioná-la com o número máximo de caracteres que o título de uma lista pode ter.

class List < ActiveRecord::Base
  TITLE_MAX_LENGTH = 100
 
  validates_presence_of :title
end
 

A falha #1 valida um relacionamento com o modelo Task, que ainda não foi criado, então vamos deixá-lo ali por enquanto. Execute novamente as especificações para ver o que precisa ser corrigido.

1)
'List should have many tasks' FAILED
expected no Exception, got #<NoMethodError: undefined method `tasks' for #<List:0x230bd38>>
./spec/models/list_spec.rb:37:
 
2)
'List should be invalid with long titles' FAILED
expected invalid?(:title) to return true, got false
./spec/models/list_spec.rb:24:
./spec/models/list_spec.rb:22:
 
Finished in 0.154639 seconds
 
5 examples, 2 failures

Temos agora, que adicionar algum código que corrija a falha #2; isso pode ser feito se adicionarmos uma chamada ao método validates_length_of.

class List < ActiveRecord::Base
  TITLE_MAX_LENGTH = 100
 
  validates_presence_of :title
  validates_length_of :title, :maximum => TITLE_MAX_LENGTH
end
 

O próximo passo é criar um modelo chamado Task. Crie tal modelo com o comando abaixo.

script/generate rspec_model Task title:string completed_at:datetime list:references

Agora, temos de definir quais são os requisitos deste nosso novo modelo.

  • Uma tarefa deve ter um título
  • Uma tarefa pode ser marcada como completada e deve marcar a hora que isso aconteceu
  • Uma tarefa deve estar associada a uma lista

Baseado nestes requisitos, podemos ter as seguintes especificações:

require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
 
describe Task do
  fixtures :tasks, :lists
 
  it "should be valid" do
    lambda {
      list = create_task
      list.should be_valid
      violated "#{list.errors.full_messages.to_sentence}" if list.new_record?
    }.should change(Task, :count)
  end
 
  it "should be invalid without a title" do
    lambda {
      task = create_task(:title => nil)
      task.errors.should be_invalid(:title)
    }.should_not change(Task, :count)
  end
 
  it "should be invalid without a list" do
    lambda {
      task = create_task(:list => nil)
      task.errors.should be_invalid(:list)
    }.should_not change(Task, :count)
  end
 
  it "should update completed_at when is done" do
    @now = Time.now
    Time.should_receive(:now).at_least(:once).and_return(@now)
 
    task = create_task
    task.complete!.should be_true
    task.completed_at.to_s.should == @now.to_s
  end
 
  it "should be completed" do
    task = create_task
    task.complete!
    task.should be_completed
  end
 
  it "should belong to a list" do
    lambda { tasks(:milk).list }.should_not raise_error
  end
 
  private
    def create_task(options={})
      Task.create({
        :title => "Milk",
        :list => lists(:supermarket)
      }.merge(options))
    end
end
 
 

E o nosso arquivo de fixtures:

milk:
  title: Milk
  list: supermarket
 
export_database:
  title: Export database
  list: work

Ele possui muita semelhança com as especificações criadas para o modelo List, mas a linha Time.should_receive(:now).at_least(:once).and_return(@now) pode confundir os mais desatentos. O método should_receive espera que o método especificado seja chamado (no nosso caso, pelo menos uma vez) e deve retornar o objeto @now. Isso é muito importante de ser feito, principalmente quando você precisa comparar datas e quer ter certeza que tudo está saindo como você espera. É muito importante que você defina o valor retornado antes de criar o stub, por motivos óbvios.

Para que nossas especificações sejam validas, precisamos modificar nosso modelo.

class Task < ActiveRecord::Base
  belongs_to :list
 
  validates_presence_of :title, :list, :list_id
  validates_associated :list
 
  def complete!
    update_attribute(:completed_at, Time.now)
  end
 
  def completed?
    !completed_at.blank?
  end
end
 

Agora que nosso modelo Task foi criado, podemos alterar o modelo List e adicionar o relacionamento has_many.

class List < ActiveRecord::Base
  TITLE_MAX_LENGTH = 100
 
  validates_presence_of :title
  validates_length_of :title, :maximum => TITLE_MAX_LENGTH
 
  has_many :tasks
end
 

Se você rodar os testes mais uma vez, verá que todas as especificações passaram! Agora, execute o comando rake spec:rcov para ver qual a cobertura de seu código, utilizando o RCov. Abra o arquivo "coverage/index.html" visualizar o arquivo gerado.

Cobertura de código com o RSpec

Acostume-se a utilizar o RCov: ele não é perfeito, mas funciona muito bem na maioria das vezes.

Uma outra coisa importante de se verificar é qual a proporção de testes escritos em relação ao código. Isso pode ser verificado com o comando rake stats. No nosso aplicativo, esta relação é de 3.7 linhas de teste escritos para cada linha de código, o que é excelente.

Rake stats

No Brasigo, temos trabalhado com 1:3 ± 0.3; isso significa que a proporção aceitável está entre 2.7 e 3.3 linhas de teste para cada linha de código. A cobertura atual é de 100%.

E agora?

Neste artigo você aprendeu a validar um modelo. Na segunda parte, vamos ver como testar os controllers. Já comecei a escrever e, se tudo der certo, publico até o fim da semana. Até lá!

Instalando o mod_rails (Phusion Passenger) no Mac OS X Leopard

20/05/08

Recentemente foi lançado o Phusion Passenger, módulo do Apache para deployment de aplicações Ruby on Rails[bb] e que, apesar de novo, já está sendo usado por grandes provedores de hospedagem como Dreamhost e, nacionalmente, pela Locaweb, como anunciado no blog. Mas não é só o ambiente de produção que pode se beneficiar do mod_rails.

Se configurado no ambiente de desenvolvimento, você não precisará mais iniciar um servidor específico para cada uma das aplicações que você tiver. Basta criar um arquivo de configuração VHost, definindo qual é o nome do servidor que irá responder para uma determinada aplicação e você já pode acessá-la.

Antes de começar, certifique-se de que tem o XCode instalado, pois o instalador irá usá-lo para compilar o Phusion Passenger. Você também precisa ter Apache 2 instalado. Se você possui uma versão mais antiga, lembre-se de atualizá-lo antes de continuar a instalação.

Instalando o Phusion Passenger

Para instalar o Phusion Passenger, você deve instalar a gem chamada passenger.

sudo gem install passenger

Depois, execute o comando abaixo para iniciar a instalação automática.

sudo passenger-install-apache2-module

Se tudo correr bem, você verá uma mensagem com algumas linhas que devem ser adicionadas ao seu arquivo de configuração do Apache.

Mensagem de instalação do Phusion passenger

Configurando o Apache

Agora, você precisa configurar o Apache para que suas aplicações Ruby on Rails sejam iniciadas. Abra o arquivo "/etc/apache2/httpd.conf" e adicione as seguintes configurações ao final do arquivo:

LoadModule passenger_module /Library/Ruby/Gems/1.8/gems/passenger-1.0.5/ext/apache2/mod_passenger.so
RailsSpawnServer /Library/Ruby/Gems/1.8/gems/passenger-1.0.5/bin/passenger-spawn-server
RailsRuby /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby
 
NameVirtualHost *
Include /private/etc/apache2/vhosts/*.conf

As três primeiras linhas devem ser substituídas pela sua própria configuração, aquela que apareceu quando você terminou de instalar o Phusion Passenger.

Crie o diretório onde serão adicionados os arquivos de configuração do VHost com o comando abaixo.

sudo mkdir /private/etc/apache2/vhosts

Agora, basta criar a configuração que irá identificar sua aplicação. Supondo que você tenha um projeto chamado 'spesa' —e você não tem!— no diretório '/Users/fnando/Sites', você adicionará as configurações abaixo ao arquivo "/private/etc/apache2/vhosts/spesa.conf":

<VirtualHost *>
  ServerName dev.spesa
  DocumentRoot /Users/fnando/Sites/spesa/public 
  RailsEnv development
</VirtualHost>
 
<Directory "/Users/fnando/spesa">
    AllowOverride FileInfo AuthConfig Limit Indexes
    Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
    <Limit GET POST OPTIONS>
      Order allow,deny
      Allow from all
    </Limit>
    <LimitExcept GET POST OPTIONS>
      Order deny,allow
      Deny from all
    </LimitExcept>
</Directory>

Para que você possa acessar seu projeto digitando o nome do servidor, você precisa adicioná-lo ao seu arquivo de hosts. Abra o arquivo "/etc/hosts" e adicione a linha abaixo.

127.0.0.1	dev.spesa

Você pode adicionar diversos servidores para responderem em um mesmo IP; Basta separá-los por espaço, como no exemplo abaixo.

127.0.0.1	dev.spesa dev.soundslike

Remova o arquivo ".htaccess" presente no seu diretório "public" com o comando "rm ~/Sites/spesa/public/.htaccess".

Pronto! Seu ambiente já está configurado. Basta reiniciar o Apache com o comando "sudo apachectl restart" e acessar o projeto com o nome do servidor escolhido. Se, por algum motivo, sua aplicação não puder ser iniciada, uma mensagem do Phusion Passenger irá aparecer.

Tela de erro do Phusion Passenger

Existe uma documentação bastante completa, com diversas configurações adicionais que podem ser feitas, além de soluções para problemas comuns. Vale a pena dar uma olhada!