Fábricas de modelos
Você já escreveu testes, nos quais as primeiras 15-20 linhas de cada teste são dedicadas apenas a configurar o estado do banco de dados usando vários modelos? Com fábricas de modelos, você pode extrair toda essa configuração para um arquivo dedicado e, em seguida, escrever o código mínimo para configurar o estado do banco de dados.
Ao final deste guia, você saberá:
- Como criar e usar fábricas
- Como definir estados de fábrica
- Trabalhando com relacionamentos de modelo API faker para gerar e usar dados aleatórios
Criando fábricas
As fábricas de modelos são armazenadas dentro do diretório databases/factories
. Você pode definir todas as fábricas em um único arquivo ou criar arquivos dedicados para cada modelo, a escolha é sua.
NOTA
Você pode usar o comando make:factory
para criar uma nova fábrica. O comando aceita o nome do modelo para o qual você deseja criar a fábrica.
Ao contrário de seeders ou modelos, as fábricas são declarativas por natureza, conforme mostrado no exemplo a seguir:
// database/factories/user.ts
import User from '#models/user'
import Factory from '@adonisjs/lucid/factories'
export const UserFactory = Factory.define(User, ({ faker }) => {
return {
username: faker.internet.userName(),
email: faker.internet.email(),
password: faker.internet.password(),
}
}).build()
- O método
Factory.define
aceita um total de dois argumentos. - O primeiro argumento é uma referência ao modelo Lucid. Faker.js
- Por fim, certifique-se de chamar o método
build
.
Usando fábricas
Usar fábricas é bem simples. Basta importar
o arquivo e usar as fábricas exportadas.
import { UserFactory } from '#database/factories/user'
const user = await UserFactory.create()
Para criar várias instâncias, você pode usar o método createMany
.
const users = await UserFactory.createMany(10)
Mesclando atributos
Você pode substituir o conjunto padrão de atributos usando o método .merge
. Por exemplo:
await UserFactory.merge({ email: 'test@example.com' }).create()
Ao criar várias instâncias, você pode definir uma matriz de atributos e eles serão mesclados com base em seus índices. Por exemplo:
await UserFactory.merge([{ email: 'foo@example.com' }, { email: 'bar@example.com' }]).createMany(3)
No exemplo acima
- O primeiro usuário terá o e-mail
foo@example.com
. - O segundo usuário terá o e-mail
bar@example.com
. - E o terceiro usuário usará o endereço de e-mail padrão, já que a matriz de mesclagem tem um comprimento de 2.
Estados de fábrica
Os estados de fábrica permitem que você defina variações de suas fábricas como estados. Por exemplo: Em uma fábrica Post
, você pode ter estados diferentes para representar postagens publicadas e rascunhos.
import Post from '#models/post'
import { Factory } from '@adonisjs/lucid/factories'
export const PostFactory = Factory.define(Post, ({ faker }) => {
return {
faker.lorem.sentence(),
content: faker.lorem.paragraphs(4),
status: 'DRAFT',
}
})
.state('published', (post) => (post.status = 'PUBLISHED')) // 👈
.build()
Por padrão, todas as postagens serão criadas com o status DRAFT
. No entanto, você pode aplicar explicitamente o estado published
para criar postagens com o status PUBLISHED
.
await PostFactory.apply('published').createMany(3)
await PostFactory.createMany(3)
Relacionamentos
As fábricas de modelos tornam super simples trabalhar com relacionamentos. Considere o seguinte exemplo:
export const PostFactory = Factory.define(Post, ({ faker }) => {
return {
faker.lorem.sentence(),
content: faker.lorem.paragraphs(4),
status: 'DRAFT',
}
}).build()
export const UserFactory = Factory.define(User, ({ faker }) => {
return {
username: faker.internet.userName(),
email: faker.internet.email(),
password: faker.internet.password(),
}
})
.relation('posts', () => PostFactory) // 👈
.build()
Agora, você pode criar um usuário
e seus posts
todos juntos em uma chamada.
const user = await UserFactory.with('posts', 3).create()
user.posts.length // 3
A fábrica encontrará o tipo de relacionamento inspecionando o modelo Lucid. Por exemplo: se seu modelo define um relacionamento
hasMany
emposts
, então a fábrica inferirá o mesmo.Um relacionamento primeiro precisa ser definido no modelo e então somente ele pode ser definido na Fábrica.
O Lucid encapsulará internamente todas as operações do banco de dados dentro de uma transação. Então, se uma persistência de relacionamento falhar, a persistência do modelo pai também será revertida.
Aplicando estados de relacionamento
Você também pode aplicar estados em um relacionamento passando um retorno de chamada para o método with
.
const user = await UserFactory.with('posts', 3, (post) => post.apply('published')).create()
Da mesma forma, se quiser, você pode criar algumas postagens com o estado publicado
e algumas sem ele.
const user = await UserFactory.with('posts', 3, (post) => post.apply('published'))
.with('posts', 2)
.create()
user.posts.length // 5
Finalmente, você também pode criar relacionamentos aninhados. Por exemplo: crie um usuário com duas postagens e cinco comentários para cada postagem.
const user = await UserFactory.with('posts', 2, (post) => post.with('comments', 5)).create()
Atributos de pivô
Ao criar um relacionamento muitos para muitos, você pode definir os atributos para a tabela dinâmica usando o método pivotAttributes
.
No exemplo a seguir, o modelo Usuário
tem um relacionamento muitos para muitos com o modelo Equipe
e definimos a função do usuário dentro de uma determinada equipe.
await UserFactory.with('teams', 1, (team) => {
team.pivotAttributes({ role: 'admin' })
}).create()
Você pode passar uma matriz de objetos para o método pivotAttributes
ao criar várias instâncias do relacionamento.
NOTA
O tamanho do array deve corresponder à contagem de linhas de relacionamento que você está prestes a criar.
await UserFactory.with('teams', 2, (team) => {
team.pivotAttributes([{ role: 'admin' }, { role: 'moderator' }])
}).create()
Chamadas de banco de dados stub
Em alguns casos, você pode preferir stub out das chamadas de banco de dados e apenas querer criar instâncias de modelo na memória. Isso pode ser obtido usando os métodos makeStubbed
e makeStubbedMany
.
const user = await UserFactory.with('posts', 2).makeStubbed()
console.log(user.id) // <some-id>
console.log(user.$isPersisted) // false
As chamadas stubbed nunca atingirão o banco de dados e atribuirão um id
numérico na memória às instâncias do modelo.
Personalizando o id do stub
NOTA
Quando dizemos id
, queremos dizer a chave primária de um modelo e não um atributo fixo nomeado id
.
O id do stub é apenas um contador na memória, que continua aumentando a cada chamada. Se necessário, você pode definir um método personalizado para gerar IDs de stub de uma maneira diferente.
Por exemplo: Gerando IDs como um BigInt
ao usar o tipo de dados bigInteger
do PostgreSQL.
import { Factory } from '@adonisjs/lucid/factories'
Factory.stubId((counter, model) => {
return BigInt(counter)
})
Você pode usar o hook makeStubbed
para personalizar o comportamento de geração de ID para uma fábrica individual.
import { randomUUID } from 'node:crypto'
Factory.define(Post, () => {
return {}
}).before('makeStubbed', (_, model) => {
model.id = randomUUID()
})
Contexto de tempo de execução
Toda vez que você cria uma instância de modelo de uma fábrica, um contexto de tempo de execução também é criado ao mesmo tempo. O contexto é então passado para todos os hooks, o retorno de chamada do método define
e também os relacionamentos.
Na maioria das vezes, você só quer acessar o objeto faker
do contexto. No entanto, a seguir estão as propriedades disponíveis.
- isStubbed: Um booleano para saber se a fábrica foi instanciada no modo stub.
- $trx: Um objeto de transação, sob o qual todas as operações do banco de dados são encapsuladas. Se você estiver executando consultas de banco de dados dentro dos ganchos de fábrica, certifique-se de envolvê-los também dentro da transação.
A seguir, um exemplo mostrando os retornos de chamada que recebem o contexto de tempo de execução (ctx)
.
Factory.define(User, (ctx) => {})
.before('create', (factory, model, ctx) => {})
.after('create', (factory, model, ctx) => {})
.state('admin', (model, ctx) => {})
.build()
Ganchos
A fábrica expõe os seguintes ganchos para executar ações antes
ou depois
de certos eventos. Você também pode definir vários ganchos para um único evento.
Factory.define(Post, () => {})
.before('create', () => {})
.after('create', () => {})
Ciclo de vida | Evento | Descrição |
---|---|---|
before | create | Invocado antes da consulta insert. |
after | create | Invocado depois da consulta insert. |
before | makeStubbed | Invocado antes da chamada stubbed. |
after | makeStubbed | Invocado depois da chamada stubbed. |
after | make | Invocado somente depois que a instância do modelo foi criada. Este gancho também é invocado antes dos ganchos before create e before makeStubbed. |
Conexões personalizadas
Factories permite que você defina uma conexão personalizada ou um cliente de consulta no momento em que os usa. Por exemplo:
await Factory.connection('tenant-1').create()
Além disso, você pode passar uma instância de cliente de consulta personalizada.
const queryClient = Database.connection('tenant-1')
await Factory.client(queryClient).create()
Para fins de uniformidade da API entre as factories e os modelos Lucid, você também pode definir a connection
ou o client
usando o método query
.
await Factory.query({ connection: 'tenant-1' }).create()
Personalizações
Finalmente, você pode personalizar opcionalmente o comportamento de certas operações realizadas nos bastidores.
newUp
Ao definir o manipulador newUp
, você pode personalizar o processo de instanciação de uma instância de modelo para uma factory específica.
Factory.define(User, () => {})
.newUp((attributes, ctx) => {
const user = new User()
user.fill(attributes)
return user
})
.build()
merge
Ao definir o manipulador merge
, você pode personalizar o comportamento de mesclagem.
Factory.define(User, () => {})
.merge((user, attributes, ctx) => {
user.merge(attributes)
})
.build()