Skip to content
julho 12, 2010 / cassiomarques

Generic tests with Rspec

Suppose you have two classes that mix-in the same module. This will make both classes to have similar behaviour. If that’s the case, there is a great change that their tests are also very similar and, probably, there will be some code duplication. When writing tests, I do not always follow the DRY principles, since I believe tests have to be as clear and simple as possible and being too smart to eliminate duplication can make the code obfuscated. But in certain cases it’s worth to care with our code and clean up our tests.

I’ll use a very simple example to make things easier. Imagine we have two classes, Line e Text. Both represent graphical elements that have to be drawn in a cartesian plane. These elements must have a rotation angle relative to the horizontal axis. A possible implementation for the tests would be the following:

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

We have a lot of duplication here. It’s possible to make things better extracting the objects initialization code to outside the it blocks:

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

It’s better, but we still have too much duplicated code. What we can do from here is to use Rspec’s shared_examples. A shared_example creates a specs group that can be shared by many other tests. It has to be used with the it_should_behave_like method:

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

The it_should_behave_like method will search for a shared specs group declared exactly with the same description, in our case “a graphic element”. What we did was extract everything that was common to both specs to an external piece of code. The shared group is executed in the same context as the describe blocks within which we used it_should_behave_like. This way, when we call element inside the tests, it uses the object created by the respective let method calls. However, we still have a point of duplication that can be improved without clarity. Note that in both describe blocks we use let in a very similar fashion to initialize the objects that will be tested. We can use the described_class method, which returns the class being tested:

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

This way the let method call will create an element of the right class for each spec.

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: