Migrando de RSpec 1.x para RSpec 2 Beta
Posted on September 26, 2010
Com o lançamento do Rails 3, estou migrando meu blog também. Para quem não lembra, estou usando uma versão (bem) modificada do Enki que é um sistema bem minimalista. Eu já mexi tanto nele que nem dá mais para dar merge, por isso também não posso publicar o código já que tem muita coisa específica do meu blog.
De qualquer forma, o Enki usar RSpec para seus testes e é o que estou usando também. A meu ver, existem muitos problemas com esses testes. Eu particularmente não gosto de usar specs de rotas, prefiro deixar isso pra testes de aceitação com Cucumber, por exemplo. Outra reclamação minha é que eu acho que o Enki usa muito mais mocks do que eu gostaria. Prefiro usar factory_girl e usar os models diretamente. Esses dois pontos deram trabalho na hora de migrar para RSpec 2. Em muitos casos ou eu retirava os testes ou trocava de mocks para factories e as coisas voltavam a funcionar.
O RSpec 2 é uma reescrita do RSpec anterior, na maior parte dos casos a API se manteve, mas algumas coisas quebraram. Vamos dar uma olhada em alguns dos pontos que eu tive que passar. Lembrando que algumas das coisas que vou dizer já haviam mudado desde o RSpec 1.3.x mas meus testes ainda estavam mais antigos do que isso.
Para começar, eu migrei minha aplicação para o mínimo do Rails 3. Você pode ver como fazer isso comprando o Rails Upgrade Handbook do Jeremy McNally ou assistir os screencasts do Ryan Bates a respeito. Depois farei um artigo com alguns dos problemas que eu tive. Antes de mais nada, retirando um mito: não é fácil migrar de Rails 2 para Rails 3. Não é “difícil” também, eu diria que é mais “trabalhoso”. E se você não tem uma boa cobertura de testes, recomendo que antes de tentar migrar primeiro crie essa cobertura, caso contrário você vai bater mais cabeça do que deveria.
Para instalar o RSpec 2 e outras gems de suporte, comece acrescentando isto no seu Gemfile:
1 2 3 4 5 6 7 8 9 10 11 |
group :test, :development, :cucumber do gem 'sqlite3-ruby', :require => 'sqlite3' gem 'webrat' gem 'database_cleaner' gem 'cucumber-rails' gem 'cucumber' gem 'rspec-rails', "~> 2.0.0.beta.22" gem 'factory_girl' gem 'ruby-debug19' gem 'launchy' # So you can do Then show me the page end |
Algumas coisas específicas do meu projeto: estou usando sqlite3 para meus testes. Tire isso se você estiver usando MySQL ou outro banco. O RSpec 2 precisa ou do Webrat ou do Capybara. Ele não tem essa dependência pré-declarada portanto se você não colocar nenhum, não vai dar nenhum erro, mas alguns matchers não vão funcionar. Escolha um dos dois e declare no Gemfile. No meu caso estou usando Webrat.
Se você usar o ruby-debug, preste atenção: atualize para ruby-debug19 se estiver usando Ruby 1.9.2 como é meu caso.
Encoding
Se você tiver comparações com strings nos seus specs, algo como isto:
1 |
@cadastro.cidade.should == "São Paulo" |
Você terá uma mensagem de erro do Ruby, do tipo invalid multibyte char (US-ASCII). Para corrigir isso, na primeira linha do arquivo Ruby, coloque:
1 |
# encoding: UTF-8 |
Para entender melhor a função de encoding, leia os artigos do Yehuda Katz a respeito. Se você ainda não estudou sobre encodings no Ruby 1.9 eu já adianto: você não sabe nada sobre encodings nem sobre unicode e o assunto é mais complexo do que parece. Pior ainda: a solução de ter tudo interno como Unicode não é a melhor solução como muitos podem acreditar se vieram de Java ou outras linguagens.
Shared Examples
Existe uma funcionalidade no RSpec que permite que você reuse testes. Por exemplo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
describe "not logged in", :shared => true do it "should not set session[:logged_in]" do session[:logged_in].should be_nil end it "should render new" do response.should be_success response.should render_template("new") end it "should set flash.now[:error]" do flash.now[:error].should_not be_nil end end ... describe "with valid URL http://enkiblog.com and OpenID authentication returning 'missing'" do before do stub_open_id_authenticate("http://enkiblog.com", :missing, true) post :create, :openid_url => "http://enkiblog.com" end it_should_behave_like "not logged in" end |
Ou seja, declarando como describe “…”, :shared => true, você pode reusar os testes com it_should_behave_like “…”. A sintaxe no RSpec 2 mudou para esta forma:
1 2 3 |
shared_examples_for "not logged in" do ... end |
Specs de Controller e de View
O pessoal do RSpec não recomenda, mas eu pessoalmente prefiro ter a chance de testar o resultado da renderização de views dentro dos testes de controllers. Isso vem desabilitado por padrão, para habilitar, bastava declarar no começo no teste de controller assim:
1 2 3 |
describe Admin::PagesController do integrate_views ... |
Agora no RSpec 2 você só muda para:
1 2 3 |
describe Admin::PagesController do render_views ... |
Além disso, temos no RSpec também testes separados apenas para Views, normalmente na pasta spec/views. Dentro dela teríamos coisas como esta:
1 2 3 4 5 6 7 8 |
describe "/admin/posts/new.html.erb" do it "should render" do assigns[:post] = Post.new template.stub!(:allow_login_bypass?).and_return(true) render '/admin/posts/new.html.erb' response.should =~ /some text/ end end |
A sintaxe muda para:
1 2 3 4 5 6 7 8 9 10 11 12 |
describe "/admin/posts/new.html.erb" do it "should render" do # trocar 'assigns' pelo método 'assign' assign(:post, Post.new) # trocar 'template' por 'view' view.stub!(:allow_login_bypass?).and_return(true) # não precisa duplicar porque o template já está declarado no "describe" render # para o conteúdo renderizado, mudar de "response" para "rendered" rendered.should =~ /some text/ end end |
Falando no response, para testar o código de retorno de uma requisição, tínhamos que fazer assim:
1 |
response.status.should == '405 Method Not Allowed'
|
Agora, em vez de comparar a string, devemos comparar o código numérico:
1 |
response.status.should == 405
|
Spec de Rota
Como disse antes, eu particularmente não gosto muito de testes de rota. De qualquer forma, às vezes há situações onde eles são úteis. No RSpec antigo, podíamos fazer assim:
1 2 3 4 5 6 7 |
it 'maps show' do route_for(:controller => 'admin/sessions', :action => 'show').should == "/admin/session" end it 'generates show params' do params_from(:get, "/admin/session").should == {:controller => 'admin/sessions', :action => 'show'} end |
O primeiro checa se o hash passado consegue derivar para a rota. O segundo checa se a rota consegue ser decomposto no hash de parâmetros requerido.
No Rspec novo ambos route_for e params_from não existem mais, portanto todo teste de rotas precisa ser reescrito. Agora estou chutando, mas no test/unit seria o equivalente, respectivamente, ao assert_generates e assert_recognizes. Você pode chamar essas asserções separadamente ou testar ambos juntos usando o assert_routing, que combina os dois.
No Rspec 2 temos o matcher route_to, que justamente chama o assert_routing. Portanto poderíamos substituir os dois testes acima por apenas um, assim:
1 2 3 |
it 'routes correctly' do {:get => "/admin/session"}.should route_to(:controller => 'admin/sessions', :action => 'show') end |
Porém, eu encontrei um problema no caso de rotas ambíguas. Por exemplo, o hash { :controller => “posts”, :action => “index” } serve tanto para a rota / (se for o “root”) quanto para /posts. Daí o route_to se confunde e eu ainda não entendi qual é a forma correta de testar. No caso, estou fazendo apenas isto:
1 |
{:get => "/"}.should be_routable
|
Onde o matcher be_routable checa se a rota funciona.
Miscelânea
Uma coisa que não investiguei o porque é esta comparação:
1 |
response.should have_text(/#{Regexp.escape(@page.to_json)}/)
|
Parece que o matcher have_text não existe mais (?) e em seu lugar podemos fazer:
1 |
response.should contain(/#{Regexp.escape(@page.to_json)}/)
|
Outro episódio engraçado foi um teste que encontrei assim:
1 2 3 |
lambda {
post :create, :post => valid_post_attributes
}.should change(Post, :count).by(0)
|
Basicamente o teste diz que o bloco deve fazer a contagem mudar por zero, ou seja, efetivamente não mudar. Isso dá problema e o RSpec diz que o teste falhou porque “esperava uma mudança de 0 mas mudou 0”. Eu twitei a respeito e logo veio uma resposta com correção, portanto esse comportamento deve estar ok na versão Edge do RSpec neste momento.
De qualquer forma, uma maneira “mais correta”, seria escrever assim:
1 2 3 |
lambda {
post :create, :post => valid_post_attributes
}.should_not change(Post, :count)
|
Ou seja, em vez de “mudar por zero” é mais correto dizer “não deve mudar”. E para deixar a coisa um pouco mais correta segundo o jeito mais atual do RSpec, devemos fazer:
1 2 3 |
expect {
post :create, :post => valid_post_attributes
}.to change(Post, :count).by(0)
|
Qualquer dessas formas deveria funcionar na versão mais recente do RSpec.
Para testar upload com gems como Paperclip podemos usar o fixture_file_upload, até então eu usava desta forma:
1 |
@upload = Upload.create(:avatar => fixture_file_upload('rails.png')) |
Porém, no novo Rspec fui obrigado a especificar o caminho completo. Não sei se esta é a forma correta ou se estou esquecendo de configurar alguma coisa:
1 |
@upload = Upload.create(:avatar => fixture_file_upload(File.join(Rails.root,'spec/fixtures/rails.png'))) |
Outra coisa que não sei se é questão de configuração: para enviar e-mails em texto puro, eu estava usando um template com o seguinte nome: purchase.text.plain.erb e isso funcionava no Rails 2. Porém, no novo ActionMailer, tive que renomear para purchase.text.erb, e só assim voltou a funcionar.
E finalmente, esta última dica não tem a ver com o Rspec e sim com o Rails 3, mas estava impedindo alguns testes: você Não pode ter um método chamado config no seu controller porque ele é reservado ao Rails. O problema é que a mensagem de erro não será muito óbvio, então já cheque agora se você tem um método com esse nome ou se um dos módulos que faz mixin no ActionController por acaso não tenta redefinir esse método. Se tiver, você vai precisar renomear para outra coisa.
blog comments powered by Disqus
Archives
- February 12(2)
- December 11(1)
- November 11(4)
- October 11(6)
- September 11(5)
- August 11(1)
- July 11(5)
- May 11(4)
- April 11(11)
- March 11(4)
- February 11(3)
- January 11(4)
- December 10(9)
- November 10(2)
- October 10(10)
- September 10(4)
- August 10(6)
- July 10(14)
- June 10(16)
- May 10(8)
- April 10(14)
- March 10(9)
- February 10(6)
- January 10(14)
- December 09(10)
- November 09(10)
- October 09(7)
- September 09(19)
- August 09(4)
- July 09(12)
- June 09(7)
- May 09(12)
- April 09(11)
- March 09(9)
- February 09(9)
- January 09(12)
- December 08(14)
- November 08(20)
- October 08(15)
- September 08(18)
- August 08(25)
- July 08(13)
- June 08(21)
- May 08(29)
- April 08(27)
- March 08(12)
- February 08(32)
- January 08(31)
- December 07(27)
- November 07(30)
- October 07(25)
- September 07(28)
- August 07(16)
- July 07(15)
- June 07(16)
- May 07(7)
- April 07(13)
- March 07(8)
- February 07(9)
- January 07(24)
- December 06(17)
- November 06(17)
- October 06(15)
- September 06(38)




