web-dev-qa-db-fra.com

Rails: Éviter les erreurs de duplication dans Factory Girl ... est-ce que je me trompe?

Supposons que j'ai un modèle user, qui a une contrainte d'unicité sur le champ email

Si j'appelle Factory(:user) une fois que tout va bien, mais si je l'appelle une deuxième fois, il échouera avec l'erreur "L'entrée existe déjà".

J'utilise actuellement un simple assistant pour rechercher une entrée existante dans la base de données avant de créer la fabrique ... et d'appeler n'importe quelle fabrique que je crée via cette aide.

Cela fonctionne, mais ce n'est pas tout à fait élégant, et compte tenu de la fréquence de ce problème, je suppose qu'il existe une meilleure solution. Donc, y a-t-il un moyen intégré dans une fille d'usine pour return_or_create une usine, au lieu de simplement charger à l'avance avec create()? Sinon, comment la plupart des gens évitent-ils les doublons avec leurs usines?

35
PlankTon

Réponse simple: utilisez factory.sequence

Si vous avez un champ qui doit être unique, vous pouvez ajouter une séquence dans factory_girl pour vous assurer que ce n'est jamais pareil:

Factory.define :user do |user|
  sequence(:email){|n| "user#{n}@factory.com" }
  user.password{ "secret" }
end

Cela incrémentera n à chaque fois afin de produire une adresse électronique unique telle que `[email protected]. (Voir https://github.com/thoughtbot/factory_girl/wiki/Usage pour plus d'informations)

Cependant, ce n'est pas toujours génial dans Rails.env.development ...

Au fil du temps, j'ai constaté que ce n'était pas réellement le moyen le plus utile de créer des adresses électroniques uniques. La raison en est que, bien que l'usine soit toujours unique pour votre environnement de test, elle ne l'est pas toujours pour votre environnement de développement et que n se réinitialise lorsque vous démarrez l'environnement de haut en bas. Dans :test, ce n'est pas un problème car la base de données est effacée, mais dans :development, vous gardez tendance à garder les mêmes données pendant un certain temps. 

Vous obtenez alors des collisions et vous vous retrouvez dans l'obligation de remplacer manuellement le courrier électronique par quelque chose que vous savez être unique et ennuyant.

Souvent plus utile: utilisez un nombre aléatoire

Comme j'appelle régulièrement u = Factory :user à partir de la console, je crée plutôt un nombre aléatoire. Vous n'êtes pas assuré d'éviter les collisions, mais dans la pratique, cela n'arrive presque jamais:

Factory.define :user do |user|
  user.email {"user_#{Random.Rand(1000).to_s}@factory.com" }
  user.password{ "secret" }
end

NB: Vous devez utiliser Random.Rand plutôt que Rand () en raison d'une collision (bogue?) Dans FactoryGirl [ https://github.com/thoughtbot/factory_girl/issues/219](see ici).

Cela vous permet de créer des utilisateurs à volonté à partir de la ligne de commande, qu'il y ait déjà des utilisateurs générés en usine dans la base de données.

Supplément optionnel pour faciliter le test du courrier électronique

Lorsque vous vous lancez dans le test du courrier électronique, vous souhaitez souvent vérifier qu'une action d'un utilisateur particulier a déclenché l'envoi d'un courrier électronique à un autre utilisateur. 

Vous vous connectez en tant que Robin Hood, envoyez un courrier électronique à Maid Marion, puis accédez à votre boîte de réception pour le vérifier. Ce que vous voyez dans votre boîte de réception provient de [email protected]. Qui donc est cette personne? 

Vous devez revenir à votre base de données pour vérifier si le courrier électronique a été envoyé/reçu par qui que vous espériez. Encore une fois, c'est un peu pénible. 

Au lieu de cela, j'aime bien générer le courrier électronique en utilisant le nom de l'utilisateur Factory associé à un nombre aléatoire. Il est ainsi beaucoup plus facile de vérifier d’où viennent les choses (tout en rendant les collisions totalement improbables). En utilisant la gemme Faker ( http://faker.rubyforge.org/ ) pour créer les noms que nous obtenons:

Factory.define :user do |user|
  user.first_name { Faker::Name::first_name }
  user.last_name { Faker::Name::last_name }
  user.email {|u| "#{u.first_name}_#{u.last_name}_#{Random.Rand(1000).to_s}@factory.com" }
end

enfin, étant donné que Faker génère parfois des noms qui ne sont pas conviviaux pour les courriers électroniques (Mike O'Donnell), nous devons ajouter à la liste blanche les caractères acceptables: .gsub(/[^a-zA-Z1-10]/, '')

Factory.define :user do |user|
  user.first_name { Faker::Name::first_name }
  user.last_name { Faker::Name::last_name }
  user.email {|u| "#{u.first_name.gsub(/[^a-zA-Z1-10]/, '')}_#{u.last_name.gsub(/[^a-zA-Z1-10]/, '')}_#{Random.Rand(1000).to_s}@factory.com" }
end

Cela nous donne des courriels personnels mais uniques tels que [email protected] et [email protected]

71
Peter Nixey

Voici ce que je fais pour forcer le 'n' dans ma séquence factory girl à être identique à l'identifiant de cet objet et éviter ainsi les collisions:

En premier lieu, je définis une méthode qui recherche l’identifiant suivant dans app/models/user.rb:

def self.next_id
  self.last.nil? ? 1 : self.last.id + 1
end 

Ensuite, j'appelle User.next_id à partir de spec/factories.rb pour lancer la séquence:

factory :user do
  association(:demo)
  association(:location)
  password  "password"
  sequence(:email, User.next_id) {|n| "darth_#{n}@sunni.ru" }
end
11
Jack Desert

J'ai trouvé cela un bon moyen d'être sûr que les tests passeront toujours ..__ Sinon, vous ne pouvez pas être sûr que 100% des fois vous allez créer un email unique.

FactoryGirl.define do
  factory :user do
    name { Faker::Company.name }
    email { generate(:email) }
  end
  sequence(:email) do
    gen = "user_#{Rand(1000)}@factory.com"
    while User.where(email: gen).exists?
      gen = "user_#{Rand(1000)}@factory.com"
    end
    gen
  end
end
3
user2593371

Si vous ne devez générer que quelques valeurs pour les attributs, vous pouvez également ajouter une méthode à String, qui garde une trace des chaînes antérieures utilisées pour un attribut. Vous pouvez ensuite faire quelque chose comme ceci:

factory :user do
  fullname { Faker::Name.name.unique('user_fullname') }
end

J'utilise cette approche pour l'ensemencement. Je voulais éviter les numéros de séquence, car ils ne semblent pas réalistes.

Voici l'extension String qui rend cela possible:

class String
  # Makes sure that the current string instance is unique for the given id.
  # If you call unique multiple times on equivalent strings, this method will suffix it with a upcounting number.
  # Example:
  #     puts "abc".unique("some_attribute") #=> "abc"
  #     puts "abc".unique("some_attribute") #=> "abc-1"
  #     puts "abc".unique("some_attribute") #=> "abc-2"
  #     puts "abc".unique("other") #=> "abc"
  #
  # Internal: 
  #  We keep a data structure of the following format:
  #     @@unique_values = {
  #       "some_for_id" => { "used_string_1" : 1, "used_string_2": 2 } # the numbers represent the counter to be used as suffix for the next item
  #     }
  def unique(for_id)
    @@unique_values ||= {} # initialize structure in case this method was never called before
    @@unique_values[for_id] ||= {} # initialize structure in case we have not seen this id yet
    counter = @@unique_values[for_id][self] || 0
    result = (counter == 0) ? self : "#{self}-#{counter}"
    counter += 1
    @@unique_values[for_id][self] = counter
    return result
  end

end

Attention: Ceci ne devrait pas être utilisé pour beaucoup d'attributs, car nous suivons toutes les chaînes précédentes (optimisations possibles). 

0
Motine