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 as 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
- Usando a 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, como mostrado no exemplo a seguir:
// database/factories/index.ts
import User from 'App/Models/User'
import Factory from '@ioc:Adonis/Lucid/Factory'
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.defineaceita um total de dois argumentos. - O primeiro argumento é uma referência ao modelo Lucid.
- O segundo argumento é um retorno de chamada que retorna um objeto de propriedades a serem usadas ao persistir a instância do modelo. Certifique-se de retornar um objeto com todas as propriedades necessárias, caso contrário, o banco de dados levantará exceções
not null. - 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'
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 Factory from '@ioc:Adonis/Lucid/Factory'
import Post from 'App/Models/Post'
export const PostFactory = Factory
.define(Post, ({ faker }) => {
return {
title: 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 {
title: 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 user e seus posts todos juntos em uma chamada.
const user = await UserFactory.with('posts', 3).create()
user.posts.length // 3Pontos a serem observados
- A fábrica encontrará o tipo de relacionamento inspecionando o modelo Lucid. Por exemplo: se seu modelo define um relacionamento
hasManyemposts, 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 published e algumas sem ele.
const user = await UserFactory
.with('posts', 3, (post) => post.apply('published'))
.with('posts', 2)
.create()
user.posts.length // 5Finalmente, 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 da matriz 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 de 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 alcançado 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) // falseAs 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 stub id
NOTA
Quando dizemos id. Queremos dizer a chave primária de um modelo e não um atributo fixo nomeado id.
O stub id é apenas um contador na memória, que continua aumentando a cada chamada. Se necessário, você pode definir um método personalizado para gerar stub ids de uma maneira diferente.
Por exemplo: Gerando ids como um BigInt ao usar o tipo de dados bigInteger do PostgreSQL.
import Factory from '@ioc:Adonis/Lucid/Factory'
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.
Factory
.define(Post, () => {
return {}
})
.before('makeStubbed', (_, model) => {
model.id = uuid.v4()
})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 de banco de dados são encapsuladas. Se você estiver executando consultas de banco de dados dentro dos hooks de fábrica, certifique-se de encapsulá-las também dentro da transação.
A seguir está 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()