Claude-skill-registry FakeBuilder Generator

Gera FakeBuilders para agregados DDD usando Chance.js seguindo padrão do projeto com PropOrFactory, type augmentation e dados realistas para testes.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fakebuilder-generator" ~/.claude/skills/majiayu000-claude-skill-registry-fakebuilder-generator && rm -rf "$T"
manifest: skills/data/fakebuilder-generator/SKILL.md
source content

FakeBuilder Generator Skill

Objetivo

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.

Quando usar

Ative esta Skill quando:

  • Criar um novo agregado que precisa de fake builder para testes
  • Gerar dados de teste realistas para testes unitários ou integração
  • Implementar test fixtures com Chance.js
  • Criar builders com padrão fluent API
  • Precisar de dados fake válidos (CPF, CNPJ, emails, etc)
  • Configurar test data factories

Entradas esperadas

  • agregado
    : Classe do agregado/entidade para o qual gerar o builder
  • propriedades
    : Lista de propriedades e seus tipos
  • validacoes_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
    )

Saídas esperadas

  • Arquivo
    .fake-builder.ts
    completo e funcional
  • Type augmentation para adicionar método
    .fake()
    no agregado
  • Comentários explicativos quando necessário
  • Exemplos de uso no topo do arquivo
  • Suporte a PropOrFactory<T> para valores fixos ou funções

Estrutura do FakeBuilder

Template Base

import { 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;
  }
}

Padrões Específicos por Tipo de Dado

1. Códigos Simples (3 dígitos)

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
};

2. Códigos Sequenciais

Para centro de custo:

private _codigo: PropOrFactory<string> = (index: number) =>
  `CC${(this.baseIndex + index + 1).toString().padStart(3, '0')}`;

3. Códigos Hierárquicos

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')}`;
};

4. CPF/CNPJ Válidos

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;
}

5. Nomes Realistas

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'])}`;

6. Emails

private _email: PropOrFactory<string | null> = (index: number) =>
  this.chance.email();

// Ou null por padrão se opcional
private _email: PropOrFactory<string | null> = () => null;

7. Telefones

private _telefone: PropOrFactory<string | null> = (index: number) =>
  `11${this.chance.integer({ min: 900000000, max: 999999999 })}`;

8. Enums

private _tipo: PropOrFactory<TipoPlanoContas> = (index: number) =>
  this.chance.pickone(['RECEITA', 'DESPESA']);

private _status: PropOrFactory<Status> = () => 'ATIVO';

9. Campos Opcionais

private _descricao: PropOrFactory<string | null> = () => null;

// Ou com valor gerado
private _descricao: PropOrFactory<string | null> = (index: number) =>
  this.chance.sentence({ words: 10 });

10. Relacionamentos (IDs)

private _parentId: PropOrFactory<string | null> = () => null;

// Para forçar relacionamento
withParentId(valueOrFactory: PropOrFactory<string>) {
  this._parentId = valueOrFactory;
  return this;
}

Chance.js - Métodos Úteis

// 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

Exemplos Completos

Exemplo 1: Banco (Simples)

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;
  }
}

Exemplo 2: PlanoContas (Hierárquico)

Uso dos FakeBuilders

Single Entity

const banco = Banco.fake()
  .anEntity()
  .build();

Multiple Entities

const bancos = Banco.fake()
  .theEntities(5)
  .build();

Custom Properties

const banco = Banco.fake()
  .anEntity()
  .withCodigo('001')
  .withNome('Banco do Brasil')
  .build();

Factory Functions

const bancos = Banco.fake()
  .theEntities(10)
  .withCodigo((index) => (index + 1).toString().padStart(3, '0'))
  .withEmpresaId(() => 'same-empresa-id')
  .build();

Test Data Setup

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);
  });
});

Checklist de Implementação

Ao gerar um FakeBuilder, SEMPRE inclua:

Estrutura Base

  • Import de Chance
  • Import do agregado e suas props
  • Type
    PropOrFactory<T>
  • Classe com TBuild generic
  • Propriedades privadas com factories
  • Constructor privado
  • Métodos estáticos
    anEntity()
    e
    theEntities()
  • Métodos
    withX()
    para cada propriedade
  • Método
    build()
  • Método auxiliar
    callFactory()

Type Augmentation

  • Adicionar método
    .fake()
    ao agregado
  • Declare module correto
  • Export do tipo correto

Dados Realistas

  • Usar Chance.js apropriadamente
  • CPF/CNPJ válidos quando necessário
  • Códigos únicos usando baseIndex
  • Emails válidos
  • Nomes em português quando apropriado

Índice Global

  • Static globalIndex para evitar colisões
  • baseIndex calculado no constructor
  • Incremento do globalIndex

Quando NÃO usar esta Skill

  • Para criar in-memory repositories
  • Para refatorações de código existente