Gera FakeBuilders para agregados DDD usando Chance.js seguindo padrão do projeto com PropOrFactory, type augmentation e dados realistas para testes.
Esta Skill orienta Claude Code a gerar FakeBuilders completos para agregados DDD seguindo o padrão estabelecido no projeto: Chance.js para dados fake realistas, fluent API, type augmentation e suporte a factories.
Ative esta Skill quando:
agregado: Classe do agregado/entidade para o qual gerar o builderpropriedades: Lista de propriedades e seus tiposvalidacoes_especiais: Regras específicas (CPF/CNPJ válido, códigos hierárquicos, etc)caminho_arquivo: Onde salvar o builder (geralmente [modulo]/domain/[nome].fake-builder.ts).fake-builder.ts completo e funcional.fake() no agregadoimport { Chance } from 'chance';
import { [Agregado], Create[Agregado]Props } from './[agregado].aggregate';
type PropOrFactory<T> = T | ((index: number) => T);
export class [Agregado]FakeBuilder<TBuild = any> {
private chance: Chance.Chance;
private countObjs: number;
private baseIndex: number;
private static globalIndex = 0;
// Propriedades com valores padrão usando factories
private _empresaId: PropOrFactory<string> = () => this.chance.guid();
private _propriedade1: PropOrFactory<tipo> = (index: number) => {
// Lógica de geração
};
private constructor(countObjs: number = 1) {
this.countObjs = countObjs;
this.chance = new Chance();
this.baseIndex = [Agregado]FakeBuilder.globalIndex * 100;
[Agregado]FakeBuilder.globalIndex += 1;
}
static anEntity() {
return new [Agregado]FakeBuilder<[Agregado]>(1);
}
static theEntities(countObjs: number) {
return new [Agregado]FakeBuilder<[Agregado][]>(countObjs);
}
with[Propriedade](valueOrFactory: PropOrFactory<tipo>) {
this._propriedade = valueOrFactory;
return this;
}
build(): TBuild {
const entities = new Array(this.countObjs)
.fill(undefined)
.map((_, index) => {
const props: Create[Agregado]Props = {
empresaId: this.callFactory(this._empresaId, index),
propriedade1: this.callFactory(this._propriedade1, index),
// ... outras propriedades
};
const entity = [Agregado].create(props);
return entity;
});
return this.countObjs === 1 ? (entities[0] as any) : (entities as any);
}
private callFactory(factoryOrValue: PropOrFactory<any>, index: number) {
return typeof factoryOrValue === 'function'
? factoryOrValue(index)
: factoryOrValue;
}
}
// Adicionar método estático ao agregado
[Agregado].fake = function () {
return [Agregado]FakeBuilder;
};
// Type augmentation
declare module './[agregado].aggregate' {
export interface [Agregado] {
fake?: typeof [Agregado]FakeBuilder;
}
namespace [Agregado] {
export let fake: () => typeof [Agregado]FakeBuilder;
}
}
Para bancos:
private _codigo: PropOrFactory<string> = (index: number) => {
const value = (this.baseIndex + index) % 900; // 0-899
return (value + 100).toString().padStart(3, '0'); // 100-999
};
Para centro de custo:
private _codigo: PropOrFactory<string> = (index: number) =>
`CC${(this.baseIndex + index + 1).toString().padStart(3, '0')}`;
Para plano de contas:
private _codigo: PropOrFactory<string> = (index: number) => {
const level1 = ((this.baseIndex + index) % 9) + 1;
const level2 = ((this.baseIndex + index) % 99) + 1;
const level3 = (index % 999) + 1;
return `${level1}.${level2.toString().padStart(2, '0')}.${level3.toString().padStart(3, '0')}`;
};
Para clientes/fornecedores:
private _documento: PropOrFactory<string> = (index: number) => {
return this._tipoPessoa === 'PF'
? this.generateValidCPF()
: this.generateValidCNPJ();
};
private generateValidCPF(): string {
const digits = Array.from({ length: 9 }, () =>
this.chance.integer({ min: 0, max: 9 })
);
const d1 = this.calculateCPFDigit(digits, 10);
const d2 = this.calculateCPFDigit([...digits, d1], 11);
return [...digits, d1, d2].join('');
}
private calculateCPFDigit(digits: number[], weight: number): number {
const sum = digits.reduce((acc, digit, idx) => {
return acc + digit * (weight - idx);
}, 0);
const remainder = sum % 11;
return remainder < 2 ? 0 : 11 - remainder;
}
private generateValidCNPJ(): string {
const digits = Array.from({ length: 12 }, () =>
this.chance.integer({ min: 0, max: 9 })
);
const d1 = this.calculateCNPJDigit(digits, [5,4,3,2,9,8,7,6,5,4,3,2]);
const d2 = this.calculateCNPJDigit([...digits, d1], [6,5,4,3,2,9,8,7,6,5,4,3,2]);
return [...digits, d1, d2].join('');
}
private calculateCNPJDigit(digits: number[], weights: number[]): number {
const sum = digits.reduce((acc, digit, idx) => {
return acc + digit * weights[idx];
}, 0);
const remainder = sum % 11;
return remainder < 2 ? 0 : 11 - remainder;
}
Usando Chance.js:
private _nome: PropOrFactory<string> = (index: number) =>
this.chance.name();
// Para empresas
private _razaoSocial: PropOrFactory<string> = (index: number) =>
`${this.chance.company()} ${this.chance.pickone(['Ltda', 'S.A.', 'ME', 'EIRELI'])}`;
private _email: PropOrFactory<string | null> = (index: number) =>
this.chance.email();
// Ou null por padrão se opcional
private _email: PropOrFactory<string | null> = () => null;
private _telefone: PropOrFactory<string | null> = (index: number) =>
`11${this.chance.integer({ min: 900000000, max: 999999999 })}`;
private _tipo: PropOrFactory<TipoPlanoContas> = (index: number) =>
this.chance.pickone(['RECEITA', 'DESPESA']);
private _status: PropOrFactory<Status> = () => 'ATIVO';
private _descricao: PropOrFactory<string | null> = () => null;
// Ou com valor gerado
private _descricao: PropOrFactory<string | null> = (index: number) =>
this.chance.sentence({ words: 10 });
private _parentId: PropOrFactory<string | null> = () => null;
// Para forçar relacionamento
withParentId(valueOrFactory: PropOrFactory<string>) {
this._parentId = valueOrFactory;
return this;
}
// Identificadores
this.chance.guid() // UUID
this.chance.hash({ length: 10 }) // Hash aleatório
// Textos
this.chance.name() // Nome de pessoa
this.chance.company() // Nome de empresa
this.chance.email() // Email
this.chance.sentence({ words: 10 }) // Frase
this.chance.paragraph() // Parágrafo
// Números
this.chance.integer({ min: 0, max: 999 })
this.chance.floating({ min: 0, max: 100, fixed: 2 })
// Datas
this.chance.date()
this.chance.timestamp()
// Endereços
this.chance.address()
this.chance.city()
this.chance.state()
this.chance.zip()
this.chance.country()
// Telefones
this.chance.phone()
// Seleção
this.chance.pickone(['A', 'B', 'C'])
this.chance.shuffle(['A', 'B', 'C'])
// Booleanos
this.chance.bool()
this.chance.bool({ likelihood: 70 }) // 70% true
import { Chance } from 'chance';
import { Banco, CreateBancoProps } from './banco.aggregate';
type PropOrFactory<T> = T | ((index: number) => T);
export class BancoFakeBuilder<TBuild = any> {
private chance: Chance.Chance;
private countObjs: number;
private baseIndex: number;
private static globalIndex = 0;
private _empresaId: PropOrFactory<string> = () => this.chance.guid();
private _codigo: PropOrFactory<string> = (index: number) => {
const value = (this.baseIndex + index) % 900;
return (value + 100).toString().padStart(3, '0');
};
private _nome: PropOrFactory<string> = (index: number) =>
`Banco ${this.baseIndex + index + 1}`;
private constructor(countObjs: number = 1) {
this.countObjs = countObjs;
this.chance = new Chance();
this.baseIndex = BancoFakeBuilder.globalIndex * 100;
BancoFakeBuilder.globalIndex += 1;
}
static anEntity() {
return new BancoFakeBuilder<Banco>(1);
}
static theEntities(countObjs: number) {
return new BancoFakeBuilder<Banco[]>(countObjs);
}
withEmpresaId(valueOrFactory: PropOrFactory<string>) {
this._empresaId = valueOrFactory;
return this;
}
withCodigo(valueOrFactory: PropOrFactory<string>) {
this._codigo = valueOrFactory;
return this;
}
withNome(valueOrFactory: PropOrFactory<string>) {
this._nome = valueOrFactory;
return this;
}
build(): TBuild {
const entities = new Array(this.countObjs)
.fill(undefined)
.map((_, index) => {
const props: CreateBancoProps = {
empresaId: this.callFactory(this._empresaId, index),
codigo: this.callFactory(this._codigo, index),
nome: this.callFactory(this._nome, index),
};
const banco = Banco.create(props);
return banco;
});
return this.countObjs === 1 ? (entities[0] as any) : (entities as any);
}
private callFactory(factoryOrValue: PropOrFactory<any>, index: number) {
return typeof factoryOrValue === 'function'
? factoryOrValue(index)
: factoryOrValue;
}
}
Banco.fake = function () {
return BancoFakeBuilder;
};
declare module './banco.aggregate' {
export interface Banco {
fake?: typeof BancoFakeBuilder;
}
namespace Banco {
export let fake: () => typeof BancoFakeBuilder;
}
}
const banco = Banco.fake()
.anEntity()
.build();
const bancos = Banco.fake()
.theEntities(5)
.build();
const banco = Banco.fake()
.anEntity()
.withCodigo('001')
.withNome('Banco do Brasil')
.build();
const bancos = Banco.fake()
.theEntities(10)
.withCodigo((index) => (index + 1).toString().padStart(3, '0'))
.withEmpresaId(() => 'same-empresa-id')
.build();
describe('BancoService', () => {
let empresaId: string;
let bancos: Banco[];
beforeEach(() => {
empresaId = 'test-empresa-id';
bancos = Banco.fake()
.theEntities(5)
.withEmpresaId(empresaId)
.build();
});
it('should have all bancos with same empresaId', () => {
expect(bancos.every(b => b.empresaId === empresaId)).toBe(true);
});
});
Ao gerar um FakeBuilder, SEMPRE inclua:
PropOrFactory<T>anEntity() e theEntities()withX() para cada propriedadebuild()callFactory().fake() ao agregado