En esta guía vamos a aprender a usar factorías con FactoryBot para generar modelos con datos de prueba para nuestros tests o entornos de desarrollo.
Esta guía no deja de ser un resumen de la documentación oficial que podremos encontrar aquí.
Normalmente ya la tendremos instalada en nuestro proyecto, pero si no lo está, debemos añadir a nuestro Gemfile del proyecto la siguiente línea:
gem 'factory_bot_rails'Aunque lo habitual es que sólo queramos que esta gema se cargue en los entornos de development y test, por lo que será más adecuado añadirlo a un grupo que ya tengamos para ese fin o crear uno de la siguiente forma:
group :development, :test do
gem 'factory_bot_rails'
endTras lo cual instalaremos la gema con el comando habitual bundle install.
Como lo más normal sea que vayamos a usar FactoryBot para generar datos de prueba en nuestros tests, podemos integrar esta gema con RSpec para añadir métodos simples para generar los datos sin tener que escribir FactoryBot constantemente. Para eso, creamos el archivo spec/support/factory_bot.rb y añadimos el siguiente contenido:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
endDebemos asegurarnos de que ese archivo se cargue en nuestros tests. Normalmente, y si el proyecto está bien configurado, en la parte superior del archivo spec/rails_helper.rb tendremos algo parecido a
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }Faker es una gema que permite generar datos de prueba organizados por muchas categorías y que usaremos para generar nombres de prueba en nuestras factorías, por lo que también tenemos que añadirlo a nuestro proyecto junto a FactoryBot:
group :development, :test do
gem 'factory_bot_rails'
gem `faker`
endPor norma general sólo definiremos una factoría por cada modelo. Definir varias factorías para un mismo modelo con el objetivo de tener distintos tipos de estados o datos se considera una mala práctica, ya que FactoryBot proporciona herramientas para hacer ese tipo de diferencias.
Las factorías se definen en la carpeta spec/factories y tienen como base la siguiente estructura:
FactoryBot.define do
factory :user do
end
endEn el ejemplo anterior, se ha definido la factoría :user para generar datos de prueba para el modelo User. FactoryBot infiere de forma automática el nombre del modelo en base al nombre de la factoría. Se puede cambiar este comportamiento si ambos difieren, pero como no se trata de una práctica recomendada, tendrás que resolverlo por ti mismo llegado el caso.
Al igual que con el nombre del modelo, las factorías deben definirse en un archivo con el mismo nombre, en singular, del modelo. Siguiendo con el ejemplo anterior, la factoría estaría definida en spec/factories/user.rb. Hay que prestar atención a si se generan las factorías con el generador de Rails, ya que por defecto nombra los archivos en plural por si contienen varias definiciones de factorías, pero como ya hemos comentado, nosotros NO vamos a hacerlo.
Para explicar de forma clara y sencilla las opciones que disponemos con FactoryBot para definir las factorías, vamos a trabajar con ejemplos reales que bien podrían aplicarse a nuestro siguiente proyecto web. En este caso contamos en nuestra aplicación con los siguientes modelos:
class User < ApplicationRecord
# email :string
# name :string
has_many :books, inverse_of: :author, foreign_key: 'author_id'
endclass Book < ApplicationRecord
# published_date :datetime
# status :string
# title :string
belongs_to :author, class_name: 'User'
endComo seguramente ya habrás detectado, tenemos usuarios y libros cuya relación es que un usuario puede tener muchos libros y un libro tiene un solo autor.
Vamos a comenzar a definir nuestra primera factoría: la de usuarios. Para ello creamos el archivo spec/factories/user.rb y definimos la factoría en su interior:
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user_#{n}@example.org" }
name { Faker::Name.name }
end
endAquí ya podemos conocer dos partes interesantes de nuestras factorías:
-
Para definir el valor con el que debe rellenarse el campo
name, definimos el atributo y le pasamos un bloque de código con su contenido. En este ejemplo como contenido estamos usando un nombre aleatorio que nos devuelve la gema Faker. De esta forma cada vez que generemos un usuario nuevo usando la factoría tendrá nombres distintos. -
Para definir el valor del campo
emailestamos usando una secuencia. Esto es una herramienta que nos proporciona FactoryBot en la que cada vez que generemos una factoría, incrementará el valor se la secuencia y se la pasará como parámetro al bloque de código y donde nosotros ya nos encargamos de usar ese valor para generar un campo único.
Ahora que ya la tenemos definida, vamos a probarla, ¿no?. Aunque FactoryBot está pensando para usarse dentro de los tests, nada nos impide usarlo dentro de la consola de Rails en el entorno development. Ejecutamos la consola:
rails consoleY escribimos dentro:
FactoryBot.create(:user)Y tras realizar esto veremos en nuestra terminal
(0.2ms) BEGIN
User Create (42.6ms) INSERT INTO "users" ("name", "email") VALUES ($1, $2) RETURNING "id" [["name", "Perry Veum III"], ["email", "user_1@example.org"]]
(6.2ms) COMMIT
=> #<User id: 1, name: "Perry Veum III", email: "user_1@example.org">
¡Ya hemos creado nuestro primer usuario! Si invocamos más veces la factoría veremos como se vuelven a crear más usuarios con nombres aleatorios y su email incrementado de manera secuencial.
Ya tenemos creada nuestra factoría para generar usuarios de forma automatizada, pero dado nuestro modelo de datos, en el que un usuario puede tener varios libros, sería interesante que cuando generásemos un usuario con nuestra factoría también tuviésemos la oportunidad de crear y asociarle algunos libros, de esta forma, tendríamos listo nuestro usuario para hacer pruebas.
Antes de poder continuar, vamos a definir nuestra segunda factoría: la de book. Para ello creamos el archivo spec/factories/book.rb y añadimos:
FactoryBot.define do
factory :book do
title { Faker::Lorem.sentence }
end
endPor el momento esta factoría solo genera una instancia del modelo Book y le añade un título aleatorio generado con nuestra fantástica gema Faker.
A continuación, vamos a modificar la factoría de user para añadirle un atributo transient. En FactoryBot los atributos transient son atributos que podemos usar para tomar decisiones internamente a la hora de utilizar la factoría y que nos van a servir en este caso para configurar el número de libros que deseamos crear y asociar al usuario. Es importante mencionar que estos atributos transient no se pasan al modelo en su creación: sólo sirven para usarse dentro de la factoría.
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user_#{n}@example.org" }
name { Faker::Name.name }
transient do
books_count { 5 }
end
end
endMediante esta opción que acabamos de añadir, estamos definiendo el atributo books_count con un valor por defecto de 5. Este valor puede ser sobreescrito a la hora de llamar a la factoría, de manera que podemos especificar un valor y si no lo hacemos, tomará un valor por defecto de 5.
Una vez tenemos ya definido el atributo que vamos a usar, el siguiente paso es añadir la lógica para crear los libros en forma de asociación. FactoryBot tiene una serie de callbacks que podemos usar para añadir nuestra lógica: after build, before create, after create y after_stub. Nosotros vamos a usar siempre after build, ya que queremos que nuestras factorías generen datos tanto en la fase de build como en la fase de create.
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user_#{n}@example.org" }
name { Faker::Name.name }
transient do
books_count { 5 }
end
after :build do |user, evaluator|
evaluator.books_count.times do
user.books << build(:book)
end
end
end
endSi nos fijamos con detalle en el código de la factoría, hemos definido un callback de tipo after build y en su interior, usamos el método build para crear una instancia de la factoría book tantas veces como indique el atributo books_count.
Si una consola de Rails ejecutamos u = FactoryBot.build :user se generará un usuario no persistido en la base de datos con 5 libros tampoco persistidos. Si en cambio ejecutamos u = FactoryBot.create :user tendremos lo mismo pero guardado todo en la base de datos.
En FactoryBot tenemos disponible unos modificadores o traits que podemos emplear para, usando una sola factoría, generar distintas variantes del modelo con distintos valores en sus campos, haciendo innecesario definir varias factorías para un mismo modelo. Podemos definir tantos traits como queramos o necesitemos.
Siguiendo con nuestro ejemplo, quizás no tenga sentido que por defecto se creen 5 libros asociados a un usuario cuando lo creamos con la factoría. Inicialmente uno puede pensar que si no queremos ese comportamiento, podemos pasar el atributo books_count con un valor de 0 cada vez que no queramos que se creen los libros, pero esto hace que la factoría sea menos legible.
Vamos a modificar nuestra factoría para añadirle el trait with_books:
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user_#{n}@example.org" }
name { Faker::Name.name }
trait :with_books do
transient do
books_count { 5 }
end
after :build do |user, evaluator|
evaluator.books_count.times do
user.books << build(:book)
end
end
end
end
endSi nos fijamos en los cambios, hemos definido un trait de nombre :with_books y dentro de él hemos metido la definición del atributo transient y el after build. Lo hemos hecho así ya que ambas definiciones ya no tienen sentido que se hagan fuera del nuevo trait, ya que si no invocamos el trait no necesitamos que se generen los libros.
Por tanto, tenemos una factoría :user que por defecto genera una instancia del modelo User con los atributos email y name generados de manera aleatoria. Adicionalmente tenemos un trait :with_books que si lo invocamos, nos genera además 5 libros por defecto asociados al usuario y si queremos cambiar ese número, lo podemos hacer a través del transient books_count. Sencillo y potente.
Siempre hay que terminar lo que se empieza, y en nuestro caso la factoría :book la habíamos dejado incompleta, por lo que vamos a terminarla.
FactoryBot.define do
factory :book do
title { Faker::Lorem.sentence }
status { 'draft' }
trait :published do
status { 'published' }
published_date { DateTime.now }
end
after :build do |book|
book.author = build(:user) unless book.author
end
end
endComo podrás observar, definimos los atirbutos title y status con valores por defecto, en este caso un título aleatorio y un estado de borrador. Estamos generando por defecto un libro que no ha sido publicado aún. Por tanto, definimos un trait :published que nos servirá para generar un libro publicado, estableciendo el estado a publicado y añadiendo una fecha de publicación. Hasta ahora nada que no hayamos visto ya y sepamos hacer.
Por último, y esta es la parte nueva que vamos a ver en esta factoría, es la forma en la que le añadimos la relación con el autor. En la documentación de FactoryBot podrás leer que en estos casos se usa el método association, pero nosotros no lo vamos a usar para tener flexibilidad en la asignación del autor. En su lugar estamos diciendo que cree un usuario con la llamada a build siempre que no le hayamos especificado uno en concreto en el constructor. Esto nos permite asiganr de forma manual autores a nuestra factoría si necesitamos que el libro pertenezca por algún motivo a un autor en concreto.
En los ejemplos anteriores hemos usado el método FactoryBot.create y FactoryBot.build para crear las factorías, pero la gema nos proporciona algunos métodos más para trabajar con ellas. Vamos para qué sirve cada uno de ellos:
-
FactoryBot.create: crea una nueva instancia del modelo y la persiste en la base de datos. -
FactoryBot.build: crea una nueva instancia del modelo pero no la persiste en la base de datos. Este método resulta especialmente útil cuando en los tests necesitamos tener modelos completos pero no necesitamos que estén en la base de datos, como por ejemplo en los tests de modelos.
Cuando queremos invocar a la factoría mediante los métodos build o create el esquema de parámetros es el siguiente:
FactoryBot.create(name, traits, attributes).
Ejemplos:
FactoryBot.create(:user): crea un usuario sin aplicar ningún trait.FactoryBot.create(:user, name: 'Javier'): crea un usuario usando el name que se le pasa como atributo.FactoryBot.create(:user, :with_books): crea un usuario con los 5 libros por defecto asociados.FactoryBot.create(:user, :with_books, books_count: 3): crea un usuario con 3 libros asociados.FactoryBot.create(:user, :with_books, name: 'Javier', books_count: 3): crea un usuario de nombre Javier y con 3 libros asociados.
Los mismos parámetros se aplican al método build.
En esta guía hemos aprendido a definir las factorías usando atributos, secuencias, traits, transients y callbacks. También hemos visto la forma correcta de definir factorías que tienen relaciones, consiguiendo tener factorías que generan datos útiles y que una vez invocadas, generan datos con sentido y útiles para nuestras pruebas.