Génération de données de test, fixtures ou factories ?

Publié le 18 avril 2012 par Alexandre Salaun | back

Cet article est publié sous licence CC BY-NC-SA

Lors du développement de votre application Rails il est important d’avoir des jeux de données afin de pouvoir naviguer dans cette dernière ou effectuer des tests. Il y a différents outils qui vous permettent de générer ces données. Les fixtures sont l’outil par défaut pour cela. Via des fichiers Yaml vous pouvez donc avoir un jeu de données. Les factories se révèlent également utiles pour la génération de données de test. Quand est-il préférable d’utiliser l’un par rapport à l’autre, c’est ce que nous allons essayer de voir ici.

Il ne faut pas non plus oublier que les seeds servent à générer des données mais dans un registre légèrement différent. En effet, les données présentes dans les seeds sont des données en dur qui sont utiles au bon fonctionnement de l’application. Ces dernières ne seront pas modifiées par la suite.

Fixtures

Les fixtures sont disponibles par défaut dans une application Rails, il n’est donc pas nécessaire d’installer de gem. Il vous suffit d’ajouter les fichiers Yaml correspondants aux modèles que vous souhaitez tester. Pour créer des données pour un modèle Post par exemple, un fichier posts.yml sera utilisé.

post_1:
  title: Post title
  content: Lorem ipsum...
  visible: true

post_2:
  title: Post title 2
  visible: false

Il est également possible d’utiliser du code Ruby dans vos fixtures afin de générer vos données :

<% 10.times do |i| %>
post_<%= i %>:
  title: Post <%= i %> title
  content: Lorem ipsum...
  visible: true
  created_at: <%= 2.days.ago %>
<% end %>

En ce qui concerne les associations, vous avez plusieurs possibilités. En donnant le nom de votre fixture liée :

# Model Post
post_1:
  title: Post title
  content: "Lorem ipsum..."
  visible: true
  author: author_1

post_2:
  title: Post title 2
  visible: false
  linked_post: post_1

# Model Author
author_1:
  name: Test Author

On fait donc référence aux objets post_1 et author_1 via le nom donnée à ces fixtures. Il est également possible d’utiliser la méthode Fixtures.identify pour trouver l’objet associé.

# Model Post
post_1:
  title: Post title
  content: Lorem ipsum...
  visible: true
  author_id: <%= ActiveRecord::Fixtures.identify(:author_1) %>

post_2:
  title: Post title 2
  visible: false
  linked_post_id: <%= ActiveRecord::Fixtures.identify(:post_1) %>

# Model Author
author_1:
  name: Test Author

Ensuite, pour utiliser vos fixtures dans vos tests, il suffit de les charger dans votre fichier de test via la ligne suivante :

fixtures :all # Pour charger toutes les fixtures
fixtures :posts # Pour charger uniquement celles concernant le modèle Post

Enfin, lorsque vous souhaitez utiliser une fixture, vous n’avez qu’à l’utiliser comme dans le cas suivant :

assert posts(:post_1).title.blank? # => false
assert posts(:post_1).title.present? # => true

Autre information à propos des fixtures, les validations sur les modèles ne sont appliquées lors de la création de ces dernières. Vous pouvez donc avoir des objets non valides lorsque vous utilisez vos fixtures.

Les fixtures sont donc très utiles pour avoir des données correspondant aux différents cas d’utilisation. Cependant, la création des fixtures peut s’avérer longue. En effet, créer les fixtures une par une, lorsqu’il s’agit d’un projet avec de nombreux modèles, est assez coûteux en temps, d’où l’intérêt d’utiliser du code Ruby pour les générer.

Factories

Les factories sont des générateurs d’objets tandis que les fixtures sont des objets prédéfinis. C’est ce qui différencie principalement ces deux moyens de générer des données.

Contrairement aux fixtures, les factories ne sont pas intégrées par défaut dans les applications Rails, il est donc nécessaire d’installer une gem. Il en existe différentes, la plus répandue étant FactoryGirl.

FactoryGirl

FactoryGirl est donc une gem permettant de créer des données pour vos tests. Pour l’installer, il vous suffit d’ajouter la gem dans votre Gemfile puis de lancer la commande bundle install :

gem "factory_girl_rails", "~> 3.0"

Ensuite, vous pourrez créer vos factories dans le fichier factories.rb ou bien dans des fichiers séparés par modèle si vous le souhaitez. Par défaut, les factories se trouvant dans les fichiers suivants sont chargées :

test/factories.rb
spec/factories.rb
test/factories/*.rb
spec/factories/*.rb

Afin de définir des données pour le modèle Post vous pouvez donc utiliser FactoryGirl comme suit :

FactoryGirl.define do
  factory :post do
    title "Post title"
    content "Lorem ipsum..."
    visible  true
  end

  factory :not_visible , class: Post do
    title "Post title 2"
    visible  false
  end
end

Il est également possible d’utiliser d’autres champs de l’objet pour définir la valeur d’un champ :

FactoryGirl.define do
  factory :user do
    firstname "Alexandre"
    lastname "Salaun"
    email { "#{firstname}.#{lastname}@test.com".downcase }
  end

En ce qui concerne les associations, il est possible d’utiliser uniquement le nom du modèle si la factory a ce même nom ou bien en spécifiant la factory si ce n’est pas le cas :

FactoryGirl.define do
  factory :author do
    firstname "Alexandre"
    lastname "Salaun"
    active true
  end

  factory :author_inactive do
    firstname "Alexandre"
    lastname "Salaun"
    active false
  end

  factory :post do
    title "Post title"
    content "Lorem ipsum..."
    author
  end

  factory :post_2 do
    title "Post title"
    content "Lorem ipsum..."
    association :author, factory: :author_inactive
  end

L’un des avantages de FactoryGirl, par rapport aux fixtures et aussi à certaines autres gem, est la possibilité de définir des séquences, ce qui permet d’avoir des valeurs uniques.

factory :user do
  sequence(:email) {|n| "user_#{n}@test.com" } # => user_1@test.com ; la valeur de n est incrémentée à chaque appel
end

Ensuite, dans vos tests il est possible d’utiliser les factories que vous avez préalablement définies :

post = FactoryGirl.create(:post)

FactoryGirl propose différentes méthodes afin de vous aidez dans vos tests :

# build => à l'image de la méthode build de Rails, on instancie un objet Post avec les données de la factory mais l'objet n'est pas sauvé
post = FactoryGirl.build(:post)

# create => instancie un objet Post et le sauve
post = FactoryGirl.create(:post)

# attributes_for => retourne une Hash avec les attributs définis pour la factory
post_attrs = FactoryGirl.attributes_for(:post)

# build_stubbed => retourne un objet Post avec les attributs définis qui sont stubbés
post_stub = FactoryGirl.build_stubbed(:post)

Avec FactoryGirl, il est également possible d’utiliser Blueprint si cela vous convient mieux. Pour cela, il faut que vous ajoutiez les lignes suivantes dans vos tests :

require "factory_girl/syntax/blueprint"
require "factory_girl/syntax/make"

Ensuite, vous pouvez donc utilisez cette syntaxe :

User.blueprint do
  name  { "Test User" }
  email { "test@test.com" }
end

User.make(name: "Test")

Contrairement aux fixtures, pour lesquelles vous pouvez avoir des objets non valides, tous les objets créés via la méthode FactoryGirl.create() sont valides. En effet, ici les validations du modèle sont appliquées lors de la création et seul des objets valides peuvent donc être créés.

Les autres gems

Il existe différentes alternatives à FactoryGirl parmi lesquelles miniskirt ou Machinist.

MiniSkirt

MiniSkirt est une gem au comportement assez similaire à FactoyGirl. En effet, comme pour cette dernière il vous faut définir des factories dans un (ou plusieurs) fichier. Pour l’installer, il suffit d’ajouter la gem à votre Gemfile puis de lancer la commande bundle install :

gem "miniskirt"

Ensuite, vous devez ajouter miniskirt dans votre test_helper.

require "miniskirt"
require "factories" # il est nécessaire d'jouter cette ligne si vous définissez des factories dans le fichier  test/factories.rb

En ce qui concerne l’usage de cette gem, il est également possible de définir des séquences, la syntaxe est légèrement différente :

factory :user do |f|
  f.email "user%d@test.com" # %d va donc être incrémenté à chaque appel
end

Les méthodes attributes_for n’est pas disponible via MiniSkirt, pour la remplacer il vous faut utiliser la syntaxe suivante :

# attributes_for
Factory.build(:user).attributes

build_stubbed se voit reserver le même traitement et il est donc nécessaire de stubber comme vous l’auriez fait avec n’importe quelle méthode ou attribut d’un objet.

Les associations sont également définies légèrement différemment :

factory :post do |f|
  f.title "Post title"
  f.author { Factory :author }
end

Machinist

Cette gem a quant à elle un fonctionnement légèrement différent. En effet, on utilise ici Blueprint pour définir nos objets.

Pour l’installer, il faut ajouter la gem à votre Gemfile puis l’installer via bundle install :

gem 'machinist', '>= 2.0.0.beta2'

Ensuite, vous devez finaliser l’installation grâce à la commande suivante :

rails generate machinist:install

Pour définir votre object, la syntaxe est la suivante :

Post.blueprint do
  title  { "Post title" }
  content   { "Lorem ipsum..." }
end

Puis, utilisez la méthode make! pour créer cet objet.

Post.make!

Machinist permet aussi de définir des associations, des valeurs uniques pour des champs donnés… tout comme le permettent les gems précédentes.

Les factories ont donc des avantages et des inconvénients tout comme les fixtures. Elles permettent de créer un plus grand nombre de données dans un temps plus court.

Fabrication

Fabrication est une autre alternative. Elle permet la génération d’objets. Il suffit de définir des Fabricators puis vous pourrez créer vos objets dans vos tests ou même dans votre application.

Pour l’installer, il faut ajouter la gem à votre Gemfile puis lancer la commande bunde install :

gem 'fabrication'

Vous pouvez ensuite définir vos Fabricators. Ceux définis dans les fichiers suivants seront chargés par défaut :

spec/fabricators/**/*fabricator.rb
test/fabricators/**/*fabricator.rb

Les Fabricators sont définis de la manière suivante :

Fabricator(:post) do
  title "Test post"
  content "Lorem ipsum..."
end

Fabricator(:post_2, :from => :post) do
  title "Test post"
  content "Lorem ipsum..."
end

Vous donnez donc le nom souhaité à votre Fabricator puis le nom de la classe (si le nom du Fabricator n’est pas déjà celui de la classe). Ensuite, comme pour les autres outils, vous définissez vos champs.

Pour les associations, vous pouvez donner directement le nom de l’objet associé ou bien spécifié le Fabricator souhaité :

Fabricator(:author) do
  name "Author Test"
end

Fabricator(:author_2, :from => :author) do
  name "Author Test"
end

# avec le nom de l'objet
Fabricator(:post) do
  author
end

# en spécifiant le fabricator
Fabricator(:post_1) do
  author { Fabricate(:author_2) }
end

Il est également possible d’utiliser des callbacks ou des alias pour vous aider dans votre génération de données.

Enfin, pour utiliser vos générateurs, il vous faut utiliser Fabricate :

Fabricate(:post)

# en spécifiant des attributs
Fabricate(:post, :title => "Article 1")

Les méthodes build et attributes_for vous permettent également de générer un objet sans le sauver ou d’en récupérer les attributs. Et, tout comme pour FactoryGirl ou MiniSkirt, vous pouvez utiliser des séquences si vous le souhaitez.

Les usages

Les usages peuvent donc être assez différents entre fixtures et factories même si le principe de base est le même : la création de données pour effectuer des tests.

Les fixtures, avantages et inconvénients ?

Si vous avez des objets avec des valeurs spécifiques à créer il est sans doute préférable de le faire via des fixtures car vous pouvez créer vos objets un par un et donc affecter une valeur à chaque attribut. Il est bien sûr également possible de le faire via des factories mais pourquoi ajouter une gem lorsque ce que vous souhaitez faire est disponible de base ?

De plus, les fixtures peuvent aussi servir à générer des données pour votre environnement de développement.

L’un des principaux point négatif des fixtures est la difficulté à maintenir ces dernières. En effet, si vous avez un nombre important de scénarios, vous aurez donc un nombre important de fixtures et il est parfois difficile de s’y retrouver. De plus, il devient parfois difficile de savoir si telle ou telle fixture est toujours utilisée au cours de l’évolution de vos tests.

En outre, la création des fichiers Yaml pour la génération des fixtures peut être chronophage.

Les factories, avantages et inconvénients ?

Si, par contre, vous souhaitez plutôt créer une quantité importante de données les factories peuvent êtres plus utiles. En effet, les séquences par exemple permettent de s’assurer de l’unicité de valeurs alors qu’avec les fixtures il faut le vérifier lors de la création dans votre fichier yaml.

D’autre part, si pour définir un objet, vous souhaitez utiliser des valeurs d’autres champs de ce dernier, vous pouvez le faire via les factories.

L’utilisation de factories plutôt que de fixtures a également pour effet d’améliorer la vitesse de vos tests. En effet, avec les factories vous ne chargez en base que les objets dont vous aurez besoin pour le test alors qu’avec les fixtures vous ne pouvez pas spécifier quels objets charger en base. Il peut donc y avoir un gain de performance.

La création d’objet valide ou invalide peut être une contrainte déterminante dans le choix de l’outil. Comme nous l’avons vu plus tôt, les fixtures permettent de créer des objets non valides, alors que FactoryGirl crée uniquement des objets valides.

Ces deux outils peuvent être utilisés en parallèle si cela est nécessaire.

Conclusion

Les fixtures et les factories peuvent donc s’avérer complémentaires suivant les cas d’utilisation. C’est à vous de voir ce qui correspond le mieux suivant les données que vous voulez générer. Il est utile de rappeler une nouvelle fois l’utilité des tests dans votre application Rails. Différents outils de tests vous sont présentés dans les articles précédents tels que Capybara ou bien Minitest.

L’équipe Synbioz.

Libres d’être ensemble.