Benchmark entre RSpec e Shoulda
13/08/09
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.
- Permalink
- Trackback
- Comentários (13)
- Ao som de: Daphne Loves Derby – Middle Middle (New Home demo)
Textos escritos por
Comentários #
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.
esse benchmark acabou com tudo, as brigas no twitter entre vocês dois eram divertidas :D
Nando,
E sobre o Factory Girl. Pelo teus testes não teve muita diferença... acha que vale a pena?
[]´s
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.
O Factory Girl melhorou o tempo do RSpec? Será que isso é só pq é um teste sintético?
Ótimo post! Parabéns!
@ 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.
Cara, vira fazer também um exemplo com Test::Unit puro - a lentidão pode ser causada pelas asserções dele.
Lucas, taí o exemplo do Test::Unit! Mesmo fazendo class_eval para adicionar os métodos, ele foi equivalente aos resultados do RSpec.
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
e agora é que eu não sei mesmo o que usar :D
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.
essa porra do Lucas sempre agitando!
porrada nele!
F L A M E W A R!
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