Skip to content
julho 12, 2010 / cassiomarques

Testes genéricos com Rspec

Suponha que você tenha duas classes que incluam o mesmo módulo. Isso fará com que o comportamento dessas duas classes tenha pontos em comum. Se este for o caso, é grande a probabilidade de que seus testes também sejam bastante parecidos e, provavelmente, haverá duplicação entre eles. Em testes, nem sempre eu sigo a regra de ser DRY. Isso porque os testes devem ser o mais claros possível e tentar ser esperto em excesso para eliminar duplicação em tudo que é canto pode deixar o código obscuro. Mas em vários casos vale à pena ter um carinho a mais com o código dos testes.

Vou usar um exemplo bem simples para facilitar a compreensão. Imagine que temos duas classes, Line e Text. Ambas representam elementos gráficos que devem ser impressos em um plano de coordenadas cartesianas. Estes elementos devem possuir um ângulo de rotação em relação ao eixo horizontal. Uma possível implementação dos testes seria:

describe Line do
  describe "#rotation=" do
    it "sets the line's rotation" do
      line = Line.new
      line.rotation = 90
      line.instance_variable_get(:@rotation).should == 90
    end
  end

  describe "#rotation" do
    it "retrieves the line's rotation" do
      line = Line.new 
      line.rotation = 90
      line.rotation.should == 90
    end
  end
end

describe Text do
  describe "#rotation=" do
    it "sets the text's rotation" do
      text = Text.new
      text.rotation = 90
      text.instance_variable_get(:@rotation).should == 90
    end
  end

  describe "#rotation" do
    it "retrieves the text's rotation" do
      text = Text.new
      text.rotation = 90
      text.rotation.should == 90
    end
  end
end

A duplicação aqui é excessiva. É possível melhorar um pouco extraindo a inicialização dos objetos para fora dos blocos it

describe Line do
  let(:line) { Line.new }

  describe "#rotation=" do
    it "sets the line's rotation" do
      line.rotation = 90
      line.instance_variable_get(:@rotation).should == 90
    end
  end

  describe "#rotation" do
    it "retrieves the line's rotation" do
      line.rotation = 90
      line.rotation.should == 90
    end
  end
end

describe Text do
  let(:text) { Text.new }

  describe "#rotation=" do
    it "sets the text's rotation" do
      text.rotation = 90
      text.instance_variable_get(:@rotation).should == 90
    end
  end

  describe "#rotation" do
    it "retrieves the text's rotation" do
      text.rotation = 90
      text.rotation.should == 90
    end
  end
end

Ficou melhor, mas ainda está muito duplicado. O que podemos fazer a partir daqui é usar o recurso de shared_examples do Rspec. O que um shared_example faz é criar um conjunto de specs que pode ser compartilhado por vários testes. Ele deve ser usado em conjunto com o método it_should_behave_like, assim:

shared_examples_for 'a graphic element' do
  describe "#rotation=" do
    it "sets the element's rotation" do
      element.rotation = 90
      element.instance_variable_get(:@rotation).should == 90
    end
  end

  describe "#rotation" do
    it "retrieves the element's rotation" do
      element.rotation = 90
      element.rotation.should == 90
    end
  end
end

describe Line do
  let(:element) { Line.new }

  it_should_behave_like 'a graphic element'
end

describe Text do
  let(:element) { Text.new }

  it_should_behave_like 'a graphic element'
end

O it_should_behave_like vai procurar por um grupo de specs compartilhadas que tenha sido declarado usando exatamente o mesmo texto para descrição, no nosso caso “a graphic element”. O que fizemos foi extrair tudo que era código comum para um módulo externo. O grupo compartilhado é executado no mesmo contexto dos blocos describe dentro dos quais usamos o it_should_behave_like. Assim, quando dentro dos testes usamos element, é usado o objeto criado pelos respectivos métodos let. Porém, ainda temos um ponto de duplicação que pode ser melhorado sem perder clareza. Observe que nos dois blocos describe estamos usando o let de forma bastante semelhante para iniciar os objetos que serão testados. Podemos usar o método described_class, que retorna a classe sendo testada:

shared_examples_for 'a graphic element' do
  let(:element) { described_class.new }

  describe "#rotation=" do
    it "sets the element's rotation" do
      element.rotation = 90
      element.instance_variable_get(:@rotation).should == 90
    end
  end

  describe "#rotation" do
    it "retrieves the element's rotation" do
      element.rotation = 90
      element.rotation.should == 90
    end
  end
end

describe Line do
  it_should_behave_like 'a graphic element'
end

describe Text do
  it_should_behave_like 'a graphic element'
end

Dessa forma o let irá iniciar um elemento da classe correta para cada teste.

7 Comentários

Deixe um comentário
  1. Felipe Elias Philipp / jul 12 2010 4:10 pm

    Muito legal! Essa parte do shared_examples e described_class era nova pra mim.

  2. Diego Carrion / jul 12 2010 4:29 pm

    Achei um excelente exemplo de quando utilizar shared examples. So acho que nesse caso ao invés de utilizar o let, podemos utilizar subject, que por padrão é equivalente a described_class.new .

    • cassiomarques / jul 12 2010 4:41 pm

      Você está certíssimo Diego, usei assim porque é dificil imaginar um exemplo de uso para um caso simples de usar no post :)

  3. Igor Leroy / jul 12 2010 4:50 pm

    Belo post, vou tentar usar algo assim nos meus testes.

  4. Jésus Lopes / jan 14 2011 10:10 pm

    Tenho usado muito essas dicas, mandou super bem!

    Parabéns =)

  5. Renato Elias / nov 25 2011 6:51 pm

    Estava Procurando uma explicação mais simples que a oficial, e olha onde vim parar \o/

    Shared Samples é vida

Trackbacks

  1. Sobre Web » Post Topic » Ruby on Rails – Testes e reaproveitamento

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: