Skip to content
março 20, 2009 / cassiomarques

Rspec: Eliminando duplicação de códigos em specs de controllers usando shared behaviours

Um recurso bastante útil que muitos usuários do Rspec não utilizam são os shared behaviours. Com eles, é possível diminuir significantemente a duplicação de código nos testes. Neste artigo pretendo mostrar uma técnica que utiliza shared behaviours para eliminar duplicação de código nas specs de controllers

Como os shared behaviours funcionam

Suponha que você possua duas classes, Book e Notebook, e ambas devam possuir os métodos number_of_pages e read_page, entre outros. Poderiamos escrever algumas specs para definir este comportamento:

describe Book do
  it "should return its number of pages" do
    book = Book.new
    3.times { book.add_page("some content") }
    book.number_of_pages.should eql(3)
  end

  it "should be readable" do
    Book.new.should respond_to(:read_page)
  end
end

describe Notebook do
  it "should return its number of pages" do
    book = Notebook.new
    3.times { book.add_page("some content") }
    book.number_of_pages.should eql(3)
  end

  it "should be readable" do
    Notebook.new.should respond_to(:read_page)
  end
end

Impossível não perceber a tremenda duplicação de código que estamos criando. Usando shared bahaviour poderiamos consertar isso. Primeiro vamos criar o shared behaviour e em seguida adaptar as specs de cada uma de nossas classes para usá-lo.

describe "all books", :shared => true do
  it "should return its number of pages" do
    3.times { @book.add_page("some content") }
    @book.number_of_pages.should eql(3)
  end

  it "should be readable" do
    @book.should respond_to(:read_page)
  end
end

No código acima, especificamos que esta spec pode ser compartilhada em outros testes através da opçào :shared => true. Agora, reparem que no código estou usando uma variável @book. E você deve estar pensando: De onde essa porcaria está vindo? O que acontece é que quando usamos um shared behaviour em um de nossos testes, é como se tivessemos colocado aquele trecho de código diretamente dentro de nossa spec atual. Métodos, atributos de instância e tudo mais estarão disponíveis para nossa spec compartilhada. Veja abaixo como o código poderia ser reescrito para utilizar o shared behaviour:

describe Book do
  before do
    @book = Book.new
  end

  it_should_behave_like "all books"
end

describe Notebook do
  before do
    @book = Notebook.new
  end

  it_should_behave_like "all books"
end

Bem melhor, não? Apenas utilizamos um bloco before para inicializarmos uma instância @book de cada classe específica, a qual será posteriormente utilizada no shared behaviour. O método it_should_behave_like “insere” as specs definidas no shared behaviour diretamente dentro do teste atual.

Aplicando a técnica às specs de controllers

Agora, vamos usar um pouco deste novo conhecimento para simplificar specs de controllers do Rails. Imagine um controller RESTful padrão, com as operações CRUD de sempre: index, show, new, create, edit, update e destroy. Imagine a duplicação de código se tivermos vários destes controllers em nossa aplicação e formos escrever as specs para todos.

Atente-se ao fato de que ainda que estes controllers sejam “comuns” e tenham as mesmas 7 actions RESTful que muitos outros controllers de muitas outras aplicaçòes, a implementação destas actions pode variar consideravelmente. Assim, os exemplos mostrados neste artigo são apenas para ilustrar as possibilidades de se tornar o código das specs mais simples e com menos duplicação utilizando-se shared behaviours. Você deve adaptar o que for mostrado aqui para sua realidade.

Considere por exemplo uma action ‘index’ em um controller BooksController. Seus objetivos básicos poderiam ser:

  • Renderizar o template index.html.erb
  • Pesquisar todos os livros atualmente gravados no banco.
  • Disponibilizar estes livros como uma lista para a utilização na view
  • Retornar o código de status HTTP 200 a cada requisição

Uma spec para esta action poderia ser algo mais ou menos assim:

describe BooksController do
  describe "responding to GET index" do
    before do
      Book.stub!(:all).and_return(@books = [mock_model(Book)])
    end

    it "should be successful" do
      get :index
      response.should be_success
    end

    it "should render the 'index' template" do
      get :index
      response.should render_template('index')
    end
    
    it "should find all books" do
      Book.should_receive(:all).and_return(@books = [mock_model(Book), mock_model(Book)])
      get :index
    end

    it "should assign all books to the view" do
      get :index
      assigns[:books].should eql(@books)
    end
  end
end

Porém, escrever isso para cada controller seria extremamente tedioso. Pior, imagine o trabalho que daria acertar cada spec caso um comportamento seja alterado e todos os controllers da aplicação devam assimilar este novo comportamento. Isso sem falar nas outras 6 actions que não implementamos. Vamos simplificar um pouco as coisas criando um shared behaviour:

describe "an ordinary 'index' action", :shared => true do
  before do
    model(described_class).stub!(:all).and_return(@list = [mock_model(model(described_class))])
  end

  def do_get_request; get :index; end

  it "should render the 'index' template" do
    do_get_request
    response.should render_template('index')
  end

  it "should be successfull" do
    do_get_request
    response.should be_success
  end

  it "should find all objects" do
    model(described_class).should_receive(:all).and_return([mock_model(model(described_class))])
    do_get_request
  end

  it "should assign a list of objects to the view" do
    do_get_request
    assigns[symbol_for_model(described_class, :pluralize => true)].should eql(@list)
  end
end

Aqui estou usando algumas “manhas” para poder descobrir, em tempo de execução, qual o respectivo model para o controller atualmente sendo testado. Dessa forma, podemos testar coisas como “o model X deve receber a mensagem y”, qualquer que seja o controller/model, sem duplicar o código. Repare a utilização do método described_class. Esse método retorna a classe atualmente sendo descrita na nossa spec, ou seja, a classe que especificamos quando fazemos algo como:

describe SomeClass do
end

Para que isso funcione vamos precisar dos seguintes métodos auxiliares:

def extract_model_name(controller_class); controller_class.to_s.match(/(.+)Controller/)[1]; end

def model(controller_class); extract_model_name(controller_class).singularize.constantize; end

def symbol_for_model(controller_class, options = {})
  tablename = extract_model_name(controller_class).tableize
  options[:pluralize] ? tablename.to_sym : tablename.singularize.to_sym
end
  • O método extract_model_name recebe a classe do controller atualmente sendo descrito, transforma seu nome em uma string e extrai o nome do respectivo model.
  • O método model retorna a classe do respectivo model
  • O método symbol_for_model retorna um symbol que representa o respectivo model, seguindo as mesmas convençòes utilizadas para converter entre nomes de models em CamelCase e os nomes das tabelas pluralizadas. Aceita a opção :pluralize, que indica se o symbol deve ser ou não pluralizado.

Reescrevendo a spec para nossa action, agora usando o shared behaviour:

describe BooksController do
  it_should_behave_like "an ordinary 'index' action"
end

Agora é só sair usando essa técnica em todos os controllers que devam possuir comportamento similar.

Mas e as outras actions?

Eu criei um arquivo auxiliar chamado shared_behaviours_for_controllers onde criei comportamento comum para os controllers de uma de minhas aplicações. Como o código ficou um pouco longo, não vou colocar neste post, para você pode copiá-lo ou estudá-lo a partir deste gist. Eu mantenho este arquivo dentro de spec/shared_behaviours_for_controllers.rb. Você pode usar este arquivo incluindo-o em seu spec_helper.rb, tornando-o assim disponível em qualquer spec que você escreva:

require File.join(RAILS_ROOT, 'spec', 'shared_behaviours_for_controllers')

Utilizando esta técnica, a spec de um controller RESTful comum BooksController ficaria mais ou menos assim (não estou testando nada de XML, JSON, etc):

describe BooksController do
  describe "responding to GET index" do
    it_should_behave_like "an ordinary 'index' action"
  end

  describe "responding to GET show" do
    it_should_behave_like "an ordinary 'show' action"
  end

  describe "responding to GET new" do
    it_should_behave_like "an ordinary 'new' action"
  end

  describe "responding to GET edit" do
    it_should_behave_like "an ordinary 'edit' action"
  end

  describe "responding to POST create" do
    it_should_behave_like "an ordinary 'create' action"
  end

  describe "responding to PUT udpate" do
    it_should_behave_like "an ordinary 'update' action"
  end

  describe "responding to DELETE destroy" do
    it_should_behave_like "an ordinary 'destroy' action"
  end
end

3 Comentários

Deixe um comentário
  1. Dante Regis / mar 20 2009 4:29 pm

    Fantástico!

    Excelente idéia, Cássio, valeu

  2. Leandro Silva / mar 20 2009 5:12 pm

    Compartilhamento de comportamento com RSpec é loko mesmo…

  3. Carlos / mar 21 2009 3:08 am

    Legal Cássio,

    já conhecia um pouquinho de shared behaviours do Rspec, mas nunca tinha pensado dessa forma para montar os testes de controllers RESTful..

    Valeu..

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: