Comment puis-je obtenir Factory Girl de ne jamais toucher la base de données si j'appelle Factory.construire afin de rendre mes tests de contrôleur rapides?

Je suis en quête de rendre mes tests Rails plus rapides. Je n'ai que 520 tests, mais ils prennent 62 secondes pour courir en bash, et 82 secondes pour courir en Rubymine.

Comme exemple d'un test de contrôleur typique, j'utilisais ce code pour sign_in en tant que @ user et créer le @comment de base dans un CommentsController pour mes tests de contrôleur RSpec:

before(:each) do
  @user = Factory.create(:user)
  sign_in @user

  @comment = Factory.create(:comment)
end

Comme vous le savez peut-être... c'est lent. Il construit un @user, mais construit également les associations pour cet utilisateur. De même pour le @comment.

J'ai donc pensé qu'appeler {[8] } le résoudrait... mais je reçois des erreurs étranges. Par exemple, current_user renvoie nil.

Donc... J'ai décidé d'utiliser Factory.build() et de supprimer tous les filtres avant dans mon contrôleur parent. Cependant, mon journal rspec dit toujours qu'une tonne d'inserts frappent la base de données lorsque j'inspecte le journal RSPec par la suite (nous parlons de centaines de lignes de code pour seulement 3 tests!)

  before(:each) do
    @user = Factory.build(:user)
    #sign_in @user

    controller.stub(:authenticate_user!) #before_filter
    controller.stub(:add_secure_model_data) #before_filter
    controller.stub(:current_user).and_return(@user)

    @comment = Factory.build(:comment)
  end

Le triste fait est que le bloc before(:each) ci-dessus n'a aucun effet sur le test performance. Comme je l'ai découvert, appeler {[11] } appellera toujours en interne Factory.create() sur les associations d'enfants.

Voici un bloc before(:each) qui supprime efficacement les pourriels produits dans le journal RSpec. Il m'a donné un coup de pouce de performance de test 35-40%

  before(:each) do
    @user = Factory.build(:user, :role => Factory.build(:role))
    #sign_in @user

    controller.stub(:authenticate_user!)
    controller.stub(:add_secure_model_data)
    controller.stub(:current_user).and_return(@user)

    # both of these are still super slow. WTF?!
    @site_update = Factory.build(:site_update, :id => 5, :author => Factory.build(:user, :role => Factory.build(:role)))

    @comment = Factory.build(:comment,
                             :author => Factory.build(:user, :role => Factory.build(:role)),
                             :commentable => @site_update)
  end

Cela rend les tests plus rapides, mais c'est aussi laid que sin. Nous ne pouvons pas écrire sérieusement cela pour chaque test... faisons-nous? Que c'est des noix. Je ne suis pas le faire.

Je tiens également à souligner que l'une de ces lignes Factory.build() prend toujours Au sujet.15 secondes même si elles ne sont pas frapper la base de données!

L'exécution de seulement 3 tests entraîne toujours environ .3 à .35 secondes de temps pris par factory_girl par test! Je pense que c'est totalement inacceptable. Si vous supprimez les lignes Factory.build(), les tests s'exécutent en 0,00001 secondes.

Je pense que le jury est dans: factory_girl est une bibliothèque vraiment lente. Est la seule solution pour ne pas l'utiliser?

Voici mon factories.rb:

Factory.define :role do |f|
  f.name "Admin"
end

Factory.define :user do |f|
  f.first_name "Banoo"
  f.last_name "Smith"
  f.sequence(:email) { |n| "Banoo.Smith#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :admin do |f|
  f.first_name "Banoo"
  f.last_name "Smith"
  f.sequence(:email) { |n| "admin#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :course_provider do |f|
  f.first_name "Josh"
  f.last_name "Bolson"
  f.sequence(:email) { |n| "josh.bolson#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :director do |f|
  f.first_name "Director"
  f.last_name "Dude"
  f.sequence(:email) { |n| "director#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :instructor do |f|
  f.first_name "Instructor"
  f.last_name "Dude"
  f.sequence(:email) { |n| "instructor#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :trainee do |f|
  f.first_name "Trainee"
  f.last_name "Dude"
  f.sequence(:email) { |n| "trainee#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
end

Factory.define :private_message do |f|
  f.subject "Subject"
  f.content "content"
  f.is_deleted_by_sender false
  f.association :sender, :factory => :user
end

Factory.define :recipient do |f|
  f.is_read false
  f.is_deleted false
  f.association :receiver, :factory => :user
  f.association :private_message
end

Factory.define :course_template do |f|
  f.name "name"
  f.description "description"
  f.association :course_provider
end

Factory.define :site_update do |f|
  f.subject "Subject"
  f.intro "intro"
  f.content "content"
  f.association :author, :factory => :user
end

Factory.define :comment do |f|
  f.content "content"
  f.association :author, :factory => :user
  f.association :commentable, :factory => :site_update
end

Factory.define :country do |f|
  f.name "Liberty"
end

Factory.define :province do |f|
  f.name "Freedom"
  f.association :country
end

Factory.define :payment_plan do |f|
  f.name "name"
  f.monthly_amount 79
  f.audience "Enterprises"
  f.active_courses "500-2000"
end

Factory.define :company do |f|
  f.name "name"
  f.phone_number "455-323-2132"
  f.address "address"
  f.postal_code "N7G-5F4"
  f.association :province
  f.association :payment_plan
end

Factory.define :company_user do |f|
  f.first_name "Dan"
  f.last_name "Grayson"
  f.sequence(:email) { |n| "dan.grayson#{n}@gmail.com" }
  f.password "secretpassword"
  f.association :role
  f.association :company
end

Factory.define :course do |f|
  f.notes "notes"
  f.difficulty 100
  f.association :course_template
  f.association :instructor, :factory => :company_user
end

Factory.define :study_group do |f|
  f.name "name"
end

Factory.define :help_category do |f|
  f.name "name"
end

Factory.define :help_document do |f|
  f.question "question"
  f.content "content"
  f.association :category, :factory => :help_category
end

Factory.define :tag do |f|
  f.name "name"
end

Factory.define :partial_mapping do |f|
  f.from_suffix "ing"
  f.to_suffix "ing"
end

Factory.define :newsletter do |f|
  f.subject "subject"
  f.content "content"
end

Factory.define :press_contact do |f|
  f.full_name "Banoo Smith"
  f.email 'Banoo.Smith@gmail.com'
  f.phone_number "455-323-2132"
  f.address "address"
  f.postal_code "N9B-3W5"
  f.association :province
end

Factory.define :press_release do |f|
  f.headline "Headline"
  f.origin "origin"
  f.intro "intro"
  f.body "body"
  f.association :contact, :factory => :press_contact
end

Factory.define :theme do |f|

end

Et benchmark intéressant. Il faut .1 à .14 secondes en moyenne pour faire un appel de Factory.create(:user):

$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.create(:user) } } }' 
      user     system      total        real
  9.940000   0.080000  10.020000 ( 14.872736)

, Même un Factory.build(:user) prend une éternité... et c'est avec :default_strategy => :build allumé!

$ rails runner 'Benchmark.bm {|x| x.report { 100.times { Factory.build(:user) } } }'
      user     system      total        real
  9.350000   0.030000   9.380000 ( 11.798339)

Clairement, c'est la preuve que quelque chose ne va pas avec factory_girl. La solution est de s'en débarrasser ou de s'assurer qu'il utilise Factory.build. C'est la réponse.

, Puisque j'ai résolu mon problème, je me demande pourquoi Factory_girl est si populaire, et pourquoi est-il "bon sens"? On peut objectivement conclure que quels que soient les avantages peut être gagné en utilisant Factory Girl-et il y a beaucoup de belles choses à ce sujet - il ne vaut pas le coût de la performance. Je suis sûr qu'une meilleure gemme d'usine pourrait être développée qui est beaucoup plus performante... mais factory_girl est malheureusement et malheureusement pas.

Ma solution ci-dessous utilise l'instanciation d'objet de base et les stubs, et les tests continuent à passer. Je pense que l'utilisation de Ruby de base, de stubs et le remplissage manuel des valeurs d'objet par test est la "bonne" chose à faire si vous le souhaitez pour éviter les appareils et également obtenir des performances élevées lors de l'exécution des tests.

21
demandé sur Fire Emblem 2011-05-25 21:33:48

2 réponses

Eh bien, je suppose que je vais répondre à ma propre question. Je pense que c'est la bonne réponse, et peut-être que d'autres peuvent en apprendre car j'ai dû passer quelques heures à l'apprendre.

Voici comment j'ai obtenu une amélioration de la vitesse de 2000% (ou 20x):

before(:each) do
  @user = User.new
  controller.stub(:authenticate_user!)
  controller.stub(:current_user).and_return(@user)
  controller.stub(:add_secure_model_data)

  @site_update = SiteUpdate.new
  @comment = Comment.new
end

La solution est simplement de ne pas utiliser D'usines d'aucune sorte pour les tests de contrôleur (et peut-être d'autres types de tests). Je suggère seulement d'utiliser L'usine quand c'est trop douloureux pour faire autrement.

Les 3 tests s'exécutent maintenant en 0.07 secondes! Avant il était de 1,4 secondes pour exécuter les 3 tests.

Factory_girl est simplement une bibliothèque terriblement lente. Je ne sais pas ce qu'il fait, mais il n'est pas profilé correctement.

Oui, je sais que cela fait beaucoup plus que de simples MyClass.new déclarations... mais même pour un langage de script plus lent comme Ruby, la performance est beaucoup plus lente que l'instanciation de classe de base. Il doit subir une optimisation massive pour que Factory.build(:my_class) soit plus conforme à MyClass.new

Je voudrais suggérez aux implémenteurs de Factory_girl d'essayer de l'obtenir pour que son overhead ne soit pas beaucoup plus lent qu'un appel MyClass.new de base (à l'exclusion du overhead de la base de données... qui ne peut être évité). Il devrait fournir un bon moyen de construire des objets et vous ne devriez pas avoir à payer une pénalité de performance 20x pour obtenir cet avantage. Ce n'est pas un compromis acceptable.

Tout cela est vraiment dommage, car {[5] } serait bien dans les contrôleurs lorsque vous avez render_views activé à l'intérieur de vos spécifications de contrôleur. Y devrait être une motivation importante pour corriger cela.

En attendant, utilisez simplement les classes Ruby/Rails de base. Je pense que vous serez étonné de voir à quelle vitesse ils sont réellement....

17
répondu Fire Emblem 2011-05-26 14:21:03

J'ai eu le même problème que @FireEmblem et j'ai finalement réduit le problème à FactoryGirl.build. FactoryGirl.stub je ne voulais pas faire les choses tout mieux.

J'ai finalement réalisé que c'était parce qu'un de mes modèles avait une logique de validation qui faisait une requête HTTP lorsqu'un certain champ était présent. L'usine a mis une valeur dans ce domaine, donc à l'extérieur, il semblait que FactoryGirl ralentissait mes tests. En réalité, c'était le cas, mais seulement parce qu'il a déclenché la requête HTTP. Enlever une ligne d'une de mes usines éliminé la requête HTTP, provoquant une amélioration des performances 60x.

0
répondu Isaac Betesh 2013-11-08 14:35:41