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:

  • Shoulda com Factory Girl
  • Shoulda sem Factory Girl
  • RSpec com Factory Girl
  • RSpec sem Factory Girl

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.7 Ruby 1.9.1
Rspec com Factory Girl 1m35.028s 1m10.277s
Rspec sem Factory Girl 1m36.353s 1m11.002s
Shoulda com Factory Girl 8m20.456s 3m33.408s
Shoulda sem Factory Girl 8m35.973s 3m35.687s
Test::Unit com Factory Girl 1m38.559s 1m39.538s
Test::Unit sem Factory Girl 1m38.944s 1m40.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.

Comentários #

#1 Lucas Húngaro disse:
13 Ago 09, 04:37PM

O RSpec melhorou bastante em relação ao ano passado, quanto tivemos aquele monte de problemas com ele.

Hoje eu sou muito mais o Micronaut! http://github.com/spicycode/micronaut/tree/master

Como houve esse melhora absurda mudando pro Ruby 1.9.1, acredito que o código do Shoulda usa muita coisa não otimizada, porque ele é realmente bem mais simples que o código do RSpec.

#2 Flavio Duarte disse:
13 Ago 09, 04:40PM

esse benchmark acabou com tudo, as brigas no twitter entre vocês dois eram divertidas :D

#3 Juliano Oliveira disse:
13 Ago 09, 04:45PM

Nando,

E sobre o Factory Girl. Pelo teus testes não teve muita diferença... acha que vale a pena?

[]´s

#4 Carlos A. da Silva disse:
13 Ago 09, 04:50PM

Muito bom Nando,

nunca havia parado para analisar um benchmark como esse, e também nunca cheguei a pensar que havia tanta diferença entre o Shoulda e o Rspec. Fiquei impressionado.
A um tempo atrás comecei usando o Shoulda em meus projetos e depois com o Remarkable migrei para o Rspec, e hoje me sinto muito bem usando ele. Agora realmente estou certo da mudança!

Abraço.

#5 George Guimarães disse:
13 Ago 09, 04:55PM

O Factory Girl melhorou o tempo do RSpec? Será que isso é só pq é um teste sintético?

Ótimo post! Parabéns!

#6 Nando Vieira disse:
13 Ago 09, 05:21PM

@ Lucas: Vou dar uma olhada no Micronaut e talvez incluir no benchmark também!

@ Juliano: Pois é! Nos dois casos (Shoulda e RSpec), usar o Factory Girl foi melhor! Para quem gosta da sintaxe, acho que é uma boa!

@ George: O que pode estar acontecendo talvez, seja por questão de escopo (estou usando private); não sei se isso influencia e é pura especulação.

#7 Lucas Húngaro disse:
13 Ago 09, 06:12PM

Cara, vira fazer também um exemplo com Test::Unit puro - a lentidão pode ser causada pelas asserções dele.

#8 Nando Vieira disse:
13 Ago 09, 09:34PM

Lucas, taí o exemplo do Test::Unit! Mesmo fazendo class_eval para adicionar os métodos, ele foi equivalente aos resultados do RSpec.

#9 Lucas Húngaro disse:
13 Ago 09, 10:18PM

Maravilha.

Coloquei esses benchmarks no grupo de discussões do Shoulda. O problema parece ser no código que gera testes do Test::Unit a partir dos shoulds, contexts e afins (o "tradutor").

Dá pra acompanhar a thread aqui: http://groups.google.com/group/shoulda/browse_thread/thread/894caa4546943f5c

#10 RoadHouse disse:
13 Ago 09, 11:29PM

e agora é que eu não sei mesmo o que usar :D

#11 Lucas Húngaro disse:
14 Ago 09, 03:00AM

Bom, existe um problema no framework, mas não acho que seja a causa de uma suite de 600 e poucos testes levar 26 minutos pra rodar.

O benchmark mostra a criação de 20.000 testes numa só classe, o que passa longe de um cenário real (isso foi dito na lista de discussão). Mostra sim que há um problema, mas nada que vá afetar uma suite "real", com tamanho médio.

Se 20.000 testes levam pouco mais de 8 minutos, porque 600 levam 26 minutos? Tudo bem que os testes do benchmark são muito simples, mas a quantidade bem maior de testes no benchmark compensa isso tranquilamente.

Como exemplo, trabalho com uma suite de testes usando Shoulda e factory_girl, com 285 testes (todos, unitários e funcionais, tocam o banco de dados). Essa suite é executada em 85 segundos.

Correndo o risco de ser chamado de fan boy , acredito que o que acontece nesse caso é um caso extremo de mal uso das ferramentas. Nem que essa suite estivesse em RJesus ela iria rodar rapidamente.

#12 RoadHouse disse:
14 Ago 09, 03:03AM

essa porra do Lucas sempre agitando!

porrada nele!

F L A M E W A R!

#13 nofxx disse:
14 Ago 09, 08:42AM

O lance eh evitar codigo q use banco em before(:each)s ... Usando :all consegui reduzir nuns 70% o tempo dos testes, (3000 shoulds, ~ 50s C2D 8500), tva beirando 2 minutos jah...

O problema eh ter de limpar depois, pois (ainda acho q eh bug) o rspec nao roda :all em transaction.

Massa tb eh o bacon. E Object Daddy ou Machinist inves de Factory Girl (which sux too)

Deixe um comentário





Não é aceito código HTML: adicione-o no pastie.org ou paste.milk-it.net e poste apenas o link.

Se este é seu primeiro comentário, ele terá que ser aprovado antes de ser exibido.

jQuery: Dominando o framework

Você quer aprender a usar jQuery de verdade? Então chegou a hora! Neste workshop você verá como funciona este framework de JavaScript, entendendo todos os aspectos que fazem do jQuery uma das melhores ferramentas para desenvolvimento de interfaces.

Saiba mais Fechar

Conheça também o HOWTO