Benchmark entre RSpec e Shoulda


Leia em 5 minutos

Em um projeto que estou trabalhando atualmente, a suíte de testes (que utiliza Shoulda e Factory Girl) demora aproximadamente 26 minutos para ser executada. Esse tempo de execução é extremamente inaceitável, já que uma das premissas do Test-Driven Development é que sua suíte de testes seja executada o mais rápido possível!

Sem nenhum embasamento, sempre achei que o tempo excessivo se dava ao uso do Factory Girl, devido sua interação com o banco de dados. Hoje, decidi tirar a prova e fiquei surpreso com alguns números obtidos em um benchmark entre RSpec e Shoulda, como você pode conferir abaixo.

Preparando o ambiente

O primeiro passo, foi criar um aplicativo novo com apenas um único modelo chamado Post.

class CreatePosts < ActiveRecord::Migration
  def self.up
    create_table :posts do |t|
      t.string :title
      t.text :content

      t.timestamps
    end
  end

  def self.down
    drop_table :posts
  end
end

Antes de realizar o benchmark, foram executados os comandos rake db:migrate e rake db:test:prepare. Depois, foram criados branches específicos para cada um dos testes.

Foram realizados 4 tipos de teste:

Todos os testes acima foram executados no Ruby 1.8.7 (2009-06-08 patchlevel 173) e Ruby 1.9.1 ruby 1.9.1 (2009-07-16 p243 revision 24175) com Rails 2.3.3. Os tempos foram obtidos utilizando o comando time ao executar a rake task padrão com o comando time rake.

Veja abaixo como foram os testes realizados.

Shoulda com Factory Girl

A primeira medição foi feita utilizando Shoulda com Factory Girl. Para criar o objeto, foi utilizada a factory abaixo.

Factory.define :post do |f|
  f.title "Lorem ipsum dolor sit amet"
  f.content "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
            eiusmod tempor incididunt ut labore et dolore magna aliqua."
end

class PostTest < ActiveSupport::TestCase
  context "Post with defaults" do
    setup do
      @post = Factory(:post)
    end

    10_000.times do |i|
    should "do assertion #{i}" do
        assert_equal @post.title, "Lorem ipsum dolor sit amet"
      end
    end
  end

  context "Post with custom title" do
    setup do
      @post = Factory(:post, :title => "Lorem ipsum dolor sit amet FTW")
    end

    10_000.times do |i|
      should "do assertion #{i}" do
        assert_equal @post.title, "Lorem ipsum dolor sit amet FTW"
      end
    end
  end
end

Shoulda sem Factory Girl

Os testes sem Factory Girl utilizaram a abordagem de se ter um método para criar o objeto.

class PostTest < ActiveSupport::TestCase
  context "Post with defaults" do
    setup do
      @post = create_post
    end

    10_000.times do |i|
      should "do assertion #{i}" do
        assert_equal @post.title, "Lorem ipsum dolor sit amet"
      end
    end
  end

  context "Post with custom title" do
    setup do
      @post = create_post(:title => "Lorem ipsum dolor sit amet FTW")
    end

    10_000.times do |i|
      should "do assertion #{i}" do
        assert_equal @post.title, "Lorem ipsum dolor sit amet FTW"
      end
    end
  end

  private
    def create_post(options={})
      Post.create({
        :title => "Lorem ipsum dolor sit amet",
        :content => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
                    eiusmod tempor incididunt ut labore et dolore magna aliqua."
      }.merge(options))
    end
end

RSpec com Factory Girl

Para testar o RSpec com Factory Gir, foi utilizado o código abaixo.

describe Post do
  describe "with defaults" do
    before do
      @post = Factory(:post)
    end

    10_000.times do |i|
      it "should do assertion #{i}" do
        @post.title.should == "Lorem ipsum dolor sit amet"
      end
    end
  end

  describe "with custom title" do
    before do
      @post = Factory(:post, :title => "Lorem ipsum dolor sit amet FTW")
    end

    10_000.times do |i|
      it "should do assertion #{i}" do
        @post.title.should == "Lorem ipsum dolor sit amet FTW"
      end
    end
  end
end

RSpec sem Factory Girl

E aqui vão os testes escritos para RSpec sem utilizar o Factory Girl.

describe Post do
  describe "with defaults" do
    before do
      @post = create_post
    end

    10_000.times do |i|
      it "should do assertion #{i}" do
        @post.title.should == "Lorem ipsum dolor sit amet"
      end
    end
  end

  describe "with custom title" do
    before do
      @post = create_post(:title => "Lorem ipsum dolor sit amet FTW")
    end

    10_000.times do |i|
      it "should do assertion #{i}" do
        @post.title.should == "Lorem ipsum dolor sit amet FTW"
      end
    end
  end

  private
    def create_post(options={})
      Post.create({
        :title => "Lorem ipsum dolor sit amet",
        :content => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
                    eiusmod tempor incididunt ut labore et dolore magna aliqua."
      }.merge(options))
    end
end

Test::Unit com Factory Girl

class PostTest < ActiveSupport::TestCase
  def setup
    @post = Factory(:post)
  end

  10_000.times do |i|
    self.class_eval <<-TXT
      def test_assertion_#{i}
        assert_equal @post.title, "Lorem ipsum dolor sit amet"
      end
    TXT
  end
end

class PostWithCustomTitleTest < ActiveSupport::TestCase
  def setup
    @post = Factory(:post, :title => "Lorem ipsum dolor sit amet FTW")
  end

  10_000.times do |i|
    self.class_eval <<-TXT
      def test_assertion_#{i}
        assert_equal @post.title, "Lorem ipsum dolor sit amet FTW"
      end
    TXT
  end
en

Test::Unit sem Factory Girl

class PostTest < ActiveSupport::TestCase
  def setup
    @post = create_post
  end

  10_000.times do |i|
    self.class_eval <<-TXT
      def test_assertion_#{i}
        assert_equal @post.title, "Lorem ipsum dolor sit amet"
      end
    TXT
  end

  private
    def create_post(options={})
      Post.create({
        :title => "Lorem ipsum dolor sit amet",
        :content => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
                    eiusmod tempor incididunt ut labore et dolore magna aliqua."
      }.merge(options))
    end
end

class PostWithCustomTitleTest < ActiveSupport::TestCase
  def setup
    @post = create_post(:title => "Lorem ipsum dolor sit amet FTW")
  end

  10_000.times do |i|
    self.class_eval <<-TXT
      def test_assertion_#{i}
        assert_equal @post.title, "Lorem ipsum dolor sit amet FTW"
      end
    TXT
  end

  private
    def create_post(options={})
      Post.create({
        :title => "Lorem ipsum dolor sit amet",
        :content => "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
                    eiusmod tempor incididunt ut labore et dolore magna aliqua."
      }.merge(options))
    end
end

Resultados

Antes de mostrar os resultados, quero dizer que fiquei extremamente impressionado com os resultados obtidos com Shoulda no Ruby 1.9.1; houve uma diminuição de pelo menos 5 minutos em relação ao mesmo teste executado no Ruby 1.8.7. No caso do RSpec, não houve diferença significativa.

O Test::Unit saiu em desvantagem nesse benchmark; para adicionar os testes, foi utilizado o método class_eval, que é uma operação bastante dispendiosa.

E se você não aguenta mais esperar pelos números, aqui vão eles:

Ruby 1.8.7Ruby 1.9.1
Rspec com Factory Girl1m35.028s1m10.277s
Rspec sem Factory Girl1m36.353s1m11.002s
Shoulda com Factory Girl8m20.456s3m33.408s
Shoulda sem Factory Girl8m35.973s3m35.687s
Test::Unit com Factory Girl1m38.559s1m39.538s
Test::Unit sem Factory Girl1m38.944s1m40.026s

Como eu jamais podia esperar, o Shoulda é o problema e não o Factory Girl! Embora eu acredite que a implementação do Shoulda seja infinitamente mais simples que a do RSpec, ela consegue ser muito mais lenta!

Pessoalmente, nunca usei o Shoulda em meus projetos pessoais. E, com certeza, não será agora que irei utilizá-lo!

Se quiser adicionar algum benchmark, veja o código utilizado nestes testes no Github: http://github.com/fnando/benchmark-rspec-shoulda/.

Update: Adicionei os tempos para os testes usando Test::Unit.