Introdução ao property-based testing com fast-check
1. O que é Property-Based Testing e por que ele é essencial na sua lista de temas
O property-based testing representa uma mudança fundamental na forma como pensamos sobre testes de software. Diferente do approach tradicional (example-based testing), onde escrevemos casos específicos como assert(soma(2, 2) === 4), o property-based testing trabalha com propriedades invariantes que devem ser verdadeiras para todas as entradas válidas.
Enquanto os testes baseados em exemplos verificam comportamentos pontuais, o property-based testing gera centenas ou milhares de casos automaticamente, explorando combinações que um desenvolvedor humano jamais consideraria. Isso é particularmente valioso para detectar bugs em bordas de domínio, cenários de concorrência e comportamentos inesperados em sistemas complexos.
A principal vantagem está na cobertura de cenários imprevistos. Um teste tradicional pode verificar se uma função de ordenação funciona para uma lista de 3 elementos; o property-based testing testará listas de tamanhos variados, com elementos repetidos, negativos, vazias e muito mais — tudo automaticamente.
2. Instalação e configuração inicial do fast-check em projetos JavaScript/TypeScript
Para começar, instale o fast-check via npm ou yarn:
npm install --save-dev fast-check
# ou
yarn add --dev fast-check
Requisitos mínimos: Node.js 12+ e um framework de teste como Jest, Mocha ou Vitest.
A estrutura básica de um teste com fast-check segue este padrão:
import * as fc from 'fast-check';
describe('Propriedades matemáticas', () => {
it('soma deve ser comutativa', () => {
fc.assert(
fc.property(
fc.integer(),
fc.integer(),
(a, b) => a + b === b + a
)
);
});
});
Aqui, fc.property define a propriedade a ser testada, e fc.assert executa o teste com múltiplas execuções aleatórias. Por padrão, são realizadas 100 execuções, mas podemos configurar:
fc.assert(
fc.property(
fc.integer(),
fc.integer(),
(a, b) => a + b === b + a
),
{ numRuns: 1000, seed: 42 }
);
O parâmetro seed permite reproduzir exatamente a mesma sequência de testes, essencial para debugging.
3. Criando suas primeiras propriedades: geradores e asserções
O fast-check oferece geradores nativos para tipos comuns:
fc.integer()— inteiros aleatóriosfc.string()— strings aleatóriasfc.array(fc.integer())— arrays de inteirosfc.boolean()— valores booleanosfc.float()— números de ponto flutuante
Para objetos complexos, combinamos geradores com fc.tuple e fc.record:
const usuarioGenerator = fc.record({
nome: fc.string({ minLength: 1, maxLength: 50 }),
idade: fc.integer({ min: 0, max: 150 }),
email: fc.string({ minLength: 5, maxLength: 100 }),
ativo: fc.boolean()
});
Exemplo de propriedade verificando idempotência (aplicar a função duas vezes produz o mesmo resultado):
it('removerEspacosDuplicados deve ser idempotente', () => {
fc.assert(
fc.property(
fc.string(),
(str) => {
const resultado = removerEspacosDuplicados(str);
return removerEspacosDuplicados(resultado) === resultado;
}
)
);
});
4. Lidando com falhas: shrinking e debugging
Quando uma propriedade falha, o fast-check automaticamente reduz o caso ao mínimo necessário para reproduzir a falha — isso é o shrinking. Por exemplo, se uma função falha para um array de 1000 elementos, o fast-check tentará reduzir para o menor array que ainda cause a falha (talvez apenas 2 elementos).
A saída de erro típica:
Property failed after 42 tests
Seed: 123456789
Counterexample: ["abc!@#", 3]
Shrunk 12 times
Você pode usar fc.sample para visualizar dados gerados:
const amostras = fc.sample(fc.integer({ min: 1, max: 100 }), { numRuns: 5 });
console.log(amostras); // [42, 17, 89, 3, 66]
Para reproduzir falhas, use a seed exata do erro:
fc.assert(
fc.property(...),
{ seed: 123456789 }
);
5. Propriedades clássicas para testar funções puras
Reversibilidade: Uma operação deve ser reversível:
it('reverse deve ser reversível', () => {
fc.assert(
fc.property(
fc.array(fc.anything()),
(arr) => arr.reverse().reverse().toString() === arr.toString()
)
);
});
Idempotência de ordenação: Ordenar duas vezes produz o mesmo resultado:
it('sort deve ser idempotente', () => {
fc.assert(
fc.property(
fc.array(fc.integer()),
(arr) => {
const sorted = [...arr].sort((a, b) => a - b);
return sorted.toString() === [...sorted].sort((a, b) => a - b).toString();
}
)
);
});
Invariantes de transformação de strings: Verificar que uma função de normalização mantém o comprimento ou remove caracteres específicos:
it('removerPontuacao não deve alterar letras', () => {
fc.assert(
fc.property(
fc.string({ minLength: 1, maxLength: 100 }),
(str) => {
const limpa = removerPontuacao(str);
return limpa.split('').every(c => /[a-zA-Z0-9\s]/.test(c));
}
)
);
});
6. Propriedades avançadas com geradores customizados
Crie geradores customizados usando fc.map, fc.filter e fc.chain:
// Gerador de CPFs válidos (apenas formato)
const cpfGenerator = fc.string({ minLength: 11, maxLength: 11 })
.map(digits => `${digits.slice(0,3)}.${digits.slice(3,6)}.${digits.slice(6,9)}-${digits.slice(9)}`);
// Gerador de emails com restrições
const emailGenerator = fc.record({
localPart: fc.string({ minLength: 1, maxLength: 20 }),
domain: fc.constantFrom('gmail.com', 'yahoo.com', 'empresa.com.br')
}).map(({ localPart, domain }) => `${localPart}@${domain}`);
// Gerador de datas no formato ISO
const dataGenerator = fc.date({ min: new Date('2020-01-01'), max: new Date('2030-12-31') })
.map(date => date.toISOString().split('T')[0]);
Para cenários de borda, use fc.anything (qualquer tipo) e fc.constantFrom (valores específicos):
const valoresBorda = fc.constantFrom(null, undefined, NaN, Infinity, '');
7. Integrando property-based testing em testes de integração e API
Para testar endpoints REST, gere payloads aleatórios:
it('API de cadastro deve aceitar usuários válidos', async () => {
await fc.assert(
fc.asyncProperty(
fc.record({
nome: fc.string({ minLength: 1, maxLength: 100 }),
email: emailGenerator,
idade: fc.integer({ min: 18, max: 120 })
}),
async (usuario) => {
const response = await fetch('/api/usuarios', {
method: 'POST',
body: JSON.stringify(usuario),
headers: { 'Content-Type': 'application/json' }
});
return response.status === 201;
}
)
);
});
Para verificar invariantes de estado após operações encadeadas:
it('operações bancárias devem manter saldo consistente', async () => {
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 100, max: 10000 }),
fc.array(fc.integer({ min: 1, max: 1000 }), { minLength: 1, maxLength: 10 }),
async (saldoInicial, saques) => {
const saldoFinal = await executarSaques(saldoInicial, saques);
return saldoFinal >= 0 && saldoFinal <= saldoInicial;
}
)
);
});
8. Boas práticas e armadilhas comuns ao adotar fast-check
Quando NÃO usar property-based testing:
- Testes visuais (UI, renderização gráfica)
- Testes de integração complexos com dependências externas lentas
- Funcionalidades com poucas variações de entrada
Documentando propriedades: Nomeie as propriedades de forma descritiva:
it('a ordenação não deve alterar o conjunto de elementos')
it('a função de hash deve produzir saída de comprimento fixo')
Balanceando cobertura: Use property-based testing como complemento, não substituto. Combine com testes example-based para cenários críticos:
// Teste example-based para casos específicos
it('soma de 1 + 1 = 2', () => expect(soma(1, 1)).toBe(2));
// Teste property-based para propriedades gerais
it('soma deve ser comutativa', () => {
fc.assert(fc.property(fc.integer(), fc.integer(), (a, b) => soma(a, b) === soma(b, a)));
});
Lembre-se: property-based testing é uma ferramenta poderosa, mas exige pensar em termos de propriedades invariantes do seu sistema. Com prática, você descobrirá bugs que jamais encontraria com testes tradicionais.
Referências
- Documentação oficial do fast-check — Guia completo de instalação, geradores e exemplos práticos
- Property-Based Testing with fast-check (artigo técnico) — Tutorial passo a passo com exemplos reais em TypeScript
- fast-check no GitHub — Repositório oficial com issues, discussões e exemplos da comunidade
- Property-Based Testing in JavaScript (artigo do LogRocket) — Guia completo com foco em cenários práticos de front-end
- Introdução ao Property-Based Testing (artigo da Trybe) — Explicação conceitual com exemplos em JavaScript
- fast-check: geradores customizados e shrinking — Tutorial avançado sobre criação de geradores e debugging de falhas
- Property-Based Testing na prática (palestra do JSConf) — Apresentação visual com demonstrações ao vivo de shrinking e casos reais