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.