Semana passada comecei finalmente um projeto do zero usando Rails 3.1. A experiência foi novidade para mim, que por causa de uma série de legados (e também por questão de performance) estava preso no Rails 2.3, e não tive a oportunidade de ver como os testes funcionam no Rails 3.
Mas antes de chegar no assunto, vamos rever que o Rails não é puramente MVC. O “Controller” do Rails agrega coisas que deveriam ser feitas na view (basicamente, buscar o objeto para ser exibido). Para mais detalhes, ver meu post anterior.
Por esse motivo, e unicamente por este motivo, eu não acredito ser possível fazer teste unitários de controllers.
Um teste unitário deve, em teoria, testar um pedaço do sistema, isoladamente de outras partes. Como fazer um teste unitário de algo que é, essencialmente, um “glue code”, ou seja, um código que une regras de negócio (Models) e interfaces (Views)?
Antes do Rails 3, eu usava uma abordagem mais “integrada” para esses specs. No controller, eu usava a palavra-chave do rspec-rails “integrate_views”, e testava o par “controller-view”. Os specs ficavam mais ou menos assim:
describe PeopleController do integrate_views it 'should show people on "index"' do sessions[:user_id] = Factory(:user).id Factory :person, :name => "Foo Bar Baz" get :index response.should be_success response.body.should include("Foo Bar Baz") end it 'should render "new" view if validation failed" do sessions[:user_id] = Factory(:user).id post :create, :person => { } response.should render_template("new") end end
Claramente, isso não é um teste unitário, mas há um grande ganho nessa abordagem: se eu resolver mudar a variável “@users” para “@records”, e atualizar a view, não preciso mexer em nenhum spec. Na prática mesmo, eu não preciso mexer em nenhum SPEC se eu mudar o layout, adicionar mais informações na view, buscar mais registros no controller e atualizá-los na view, enfim, em qualquer momento eu sei, exatamente, se o teste está falhando ou passando, sem as fragilidades que mocks podem oferecer.
No Rails 3, porém, apareceu um novo diretório: spec/requests. No início do projeto que estou fazendo, comecei a usar o spec/controllers para fazer meus testes (com, obviamente, “render_views” no lugar de “integrate_views”), mas pouco a pouco isso se mostrou um pouco… inútil, digamos assim. Isso porque o diretório “spec/requests” pode usar o “Capybara” para fazer seus testes, o que significa que com os drivers corretos, eu posso inclusive rodar javascript, e nesse caso eu posso ter uma abordagem ainda MAIS integrada: ao invés de ir direto para o controller que eu quero testar, eu posso de fato LOGAR o usuário, ver se a ação que eu quero testar está disponível (isto é, se há links que levam até a ação), ver se o usuário tem acesso ou não (e testar mais facilmente os acessos), etc… o que me levou a, definitivamente, deixar de usar o “spec/controllers” para esse tipo de teste. O resultado final é mais ou menos assim (usando Capybara):
describe 'People' do #Refere-se ao PeopleController it 'should list people' do Factory :person, :name => "Foo Bar Baz" user = Factory :user visit root_path click_link 'Sign in' fill_in 'Login', :with => user.login fill_in 'Password', :with => user.password click_button 'Login' click_link 'People' body.should include('Foo Bar Baz') end #... mesma coisa para o "new", só que preenchendo os formulários end
Ok, só que o teste definitivamente ficou maior do que a versão do Controller. Para resolver isso, basta criar um arquivo, digamos, em “spec/support/login_helper.rb”, e colocar o código de logar usuário lá. Mais um ou outro helper, e o arquivo fica mais ou menos assim:
#arquivo spec/support/login_helper.rb def enter_on_admin_by(link) login_user click_link link end def login_user(user = Factory :user) visit root_path click_link 'Sign in' fill_in 'Login', :with => user.login fill_in 'Password', :with => user.password click_button 'Login' end
Aí o SPEC fica assim:
describe 'People' do it 'should list people' do enter_on_admin_by 'People' body.should include('Foo Bar Baz') end it 'should show errors if creating an invalid person" do enter_on_admin_by 'People' click_link 'New person' click_link 'Save' body.should match(/Name can't be blank/) end end
A maior vantagem desta abordagem é que, quando os códigos nos controllers ou views tiverem um pouco mais de peculiaridades, é possível escrever um código de teste apenas para o controller e testar aquela funcionalidade isoladamente… claro, desde que isso não introduza testes frágeis.
Enfim, como um “bonus point”, uma coisa que eu me preocupo sempre é em escrever poucas linhas de teste para fazer coisas “the rails way”, isto é, se eu for testar uma tela de cadastro “padrão”, eu não precise escrever demais. Para isso, eu fiz um helper nos meus testes:
#arquivo spec/support/crud_helper.rb def assert_created_record(element) record = Factory.stub element update_each_attribute element, record.attributes click_button 'Create' body.should include(record.attributes.values[1].to_s) end def update_each_attribute(element, attributes) attributes.each_pair do |field, value| field_name = "#{element}[#{field}]" fill_in_field field_name, value end end private :update_each_attribute def fill_in_field(field_name, value) case value when String then fill_in field_name, :with => value when true, false then check field_name end rescue Capybara::ElementNotFound end private :fill_in_field
E então, meus testes podem ficar mais simples quando eu for criar uma pessoa no formato padrão do Rails:
describe 'People' do #...outros testes... it 'should create a new person' do enter_on_admin_by 'People' click_link "New person" assert_created_record :person end end
E com isso, se for bem disciplinado, é possível identificar imediatamente “código-morto”: se cada teste em seu “spec/requests” está cobrindo uma característica de seu programa, basta rodar uma ferramenta de cobertura de código (simple_cov, por exemplo) no diretório spec/requests. Espera-se que todos os controllers estejam cobertos. Se um model tem partes que não estão cobertas, provavelmente essa parte pode ser eliminada do sistema. Até agora, só vi vantagens nessa abordagem.
Por fim, se alguma parte do sistema depender de Javascript, é possível escrever o método com:
describe 'People' do it 'should do something with ajax', :driver => :webkit do click_link "Remote call" wait_until { body =~ /Add child/ } end end
e usar a gem “capybara-webkit”, que é muito boa. Só lembrar de SEMPRE usar “wait_until”, para assegurar que a página carregou antes de continuar o teste. Outra coisa, esse tipo de abordagem exigiria desabilitar “transactional_fixtures”, o que deixa os testes BEM mais lentos. Uma alternativa seria, num arquivo como “spec/support/ar_shared_connection.rb”, colocar o código abaixo (retirado do blog da PlataformaTEC):
#Baseado no artigo da Plataforma: #http://blog.plataformatec.com.br/2011/12/three-tips-to-improve-the-performance-of-your-test-suite/ class ActiveRecord::Base mattr_accessor :shared_connection @@shared_connection = nil def self.connection @@shared_connection || retrieve_connection end end <h1>Forces all threads to share the same connection. This works on</h1> <h1>Capybara because it starts the web server in a thread.</h1> ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
Enfim, é isso. Controllers para specs bem específicos, requests para testar extensivamente o controller em abordagem mais “integrada”, e models para testar regras de negócio. Agora, só falta aprender como fazer o SPORK funcionar corretamente :).
2 Comments
Thiago Vieira · 2012-01-13 at 07:08
Muito legal, Maurício. Não sabia que spec/request poderia usar o Capybara. Atualmente estou usando spec/controller apenas para testar permissão de acesso, conferir as variáveis instanciadas e a resposta (render ou redirect). Já a parte visual da página, estou usando spec/views (inicialmente não gostei muito, mas tenho percebido que é útil). Por fim, nada como usar o Cucumber para as operações mais críticas do sistema.
Tiago Almeida · 2012-02-28 at 11:56
Legal esse post… Ajuda a perceber como a melhora da ferramenta pode nos ajudar a melhorar a suite de testes
Comments are closed.