fast-check é uma biblioteca de property-based testing para JavaScript e TypeScript. Em vez de escrever somente exemplos fixos, o teste descreve propriedades que devem valer para muitos valores gerados automaticamente. A ferramenta procura casos extremos, registra uma seed reproduzível e reduz entradas que falham até chegar a um contraexemplo pequeno.
Property-based testing complementa testes tradicionais. Exemplos continuam úteis para regras específicas, enquanto propriedades exploram combinações que o desenvolvedor talvez não tenha imaginado.
Instalação
npm install -D fast-check
Com Node Test Runner:
import test from 'node:test';
import assert from 'node:assert/strict';
import fc from 'fast-check';
test('reverter duas vezes mantém a string', () => {
fc.assert(
fc.property(fc.string(), (value) => {
const result = [...[...value].reverse()].reverse().join('');
assert.equal(result, value);
}),
);
});Arbitraries
Arbitraries geram valores e sabem reduzi-los. Exemplos:
fc.string()
fc.integer()
fc.nat()
fc.boolean()
fc.date()
fc.uuid()
fc.emailAddress()
fc.array(fc.integer())
fc.option(fc.string())
fc.record({ id: fc.uuid(), active: fc.boolean() })Use o arbitrary mais próximo do domínio. Gerar strings genéricas quando o campo é UUID desperdiça execuções em entradas inválidas.
Propriedades de uma ordenação
function ordenar(values) {
return [...values].sort((a, b) => a - b);
}
test('ordenação preserva valores e ordem', () => {
fc.assert(
fc.property(fc.array(fc.integer()), (values) => {
const result = ordenar(values);
assert.equal(result.length, values.length);
assert.deepEqual(
[...result].sort((a, b) => a - b),
result,
);
const originalCounts = new Map();
for (const value of values) {
originalCounts.set(value, (originalCounts.get(value) ?? 0) + 1);
}
for (const value of result) {
const count = originalCounts.get(value) ?? 0;
assert.ok(count > 0);
originalCounts.set(value, count - 1);
}
}),
);
});A propriedade descreve invariantes, não exemplos isolados.
Shrinking
Quando um valor grande falha, fast-check tenta simplificá-lo. Uma lista de centenas de elementos pode virar [0, 0]; uma string complexa pode virar um caractere. Esse processo torna o bug compreensível.
Não substitua arbitraries por geradores aleatórios manuais, pois perderá shrinking e reprodução.
Seed e reprodução
Uma falha exibe seed e caminho. Reproduza:
fc.assert(property, {
seed: 123456789,
path: '4:2:0',
});Depois de corrigir, adicione um teste de exemplo com o contraexemplo mínimo quando ele representa uma regressão importante. Remova seed temporária para restaurar exploração ampla.
Configuração de execuções
fc.assert(property, {
numRuns: 1000,
endOnFailure: true,
});Mais execuções aumentam cobertura probabilística e tempo. Use poucas em PRs e mais em execuções noturnas quando necessário.
Determinismo no CI
Falhas são reproduzíveis pela seed informada. Também é possível configurar seed fixa, mas usar sempre a mesma sequência reduz variedade entre execuções. Uma estratégia é deixar aleatória e registrar seed em toda falha.
Precondições
fc.property(fc.integer(), fc.integer(), (a, b) => {
fc.pre(b !== 0);
assert.equal((a / b) * b, a);
});Precondições descartam casos. Muitas rejeições tornam o teste ineficiente; prefira construir um arbitrary que gere somente valores válidos:
const nonZeroInteger = fc.integer().filter((value) => value !== 0);Mesmo filter pode rejeitar muito. Combine intervalos ou mapeamentos.
Map e chain
Crie valores derivados:
const positiveMoney = fc
.integer({ min: 0, max: 1_000_000 })
.map((cents) => ({ cents, currency: 'BRL' }));Para dependência entre campos:
const range = fc.integer({ min: 0, max: 100 }).chain((min) =>
fc.integer({ min, max: 100 }).map((max) => ({ min, max })),
);Records de domínio
const userArbitrary = fc.record({
id: fc.uuid(),
name: fc.string({ minLength: 1, maxLength: 80 }),
email: fc.emailAddress(),
roles: fc.uniqueArray(fc.constantFrom('admin', 'editor', 'viewer')),
active: fc.boolean(),
});Compartilhe arbitraries próximos ao domínio de testes, sem misturá-los ao código de produção.
Comandos e modelos
Stateful property testing gera sequências de operações e compara sistema real com modelo simplificado. É útil para caches, filas, carrinhos e máquinas de estado.
Um modelo de saldo pode executar depósitos, saques e consultas em ordens variadas. fast-check reduz a sequência até os poucos comandos que provocam inconsistência.
Propriedades úteis
- round trip: decodificar o que foi codificado retorna o original;
- idempotência: aplicar operação duas vezes equivale a uma;
- comutatividade: ordem não altera resultado quando esperado;
- invariantes: saldo não fica negativo;
- monotonicidade: adicionar itens não reduz contagem;
- equivalência: implementação nova coincide com referência;
- não quebra: parser não lança para entradas permitidas.
Testando parsers
test('JSON serializa e desserializa valores suportados', () => {
const jsonValue = fc.jsonValue();
fc.assert(
fc.property(jsonValue, (value) => {
assert.deepEqual(JSON.parse(JSON.stringify(value)), value);
}),
);
});Escolha o domínio suportado. JSON não preserva todos os valores JavaScript.
Datas e timezones
Datas geradas encontram limites, anos bissextos e fusos. Não use apenas new Date() atual. Defina intervalo válido para o negócio e normalize timezone explicitamente.
Unicode
Strings aleatórias podem conter caracteres que revelam bugs de comprimento, normalização e divisão por code unit. Use iteradores Unicode ao inverter texto e teste emojis, combinações e caracteres invisíveis quando relevantes.
Segurança
Geradores podem incluir chaves perigosas como __proto__, ajudando a encontrar prototype pollution. Crie propriedades para garantir que merge e parsing não modifiquem protótipos.
Integração com Jest ou Vitest
Use fc.assert dentro do it:
it('normalizar é idempotente', () => {
fc.assert(
fc.property(fc.string(), (value) => {
expect(normalizar(normalizar(value))).toBe(normalizar(value));
}),
);
});Testes assíncronos
test('cache devolve valor gravado', async () => {
await fc.assert(
fc.asyncProperty(fc.string(), fc.jsonValue(), async (key, value) => {
await cache.set(key, value);
assert.deepEqual(await cache.get(key), value);
}),
);
});Limpe estado entre execuções. Testes assíncronos com dependências reais podem ficar lentos.
Integração e bancos
Property-based tests podem usar Testcontainers, mas o custo cresce. Gere lotes dentro de um único container, use transações de rollback e reduza numRuns. Mantenha propriedades puras sempre que possível.
Tempo limite
Uma entrada pode provocar algoritmo muito lento. Defina limites de tamanho e timeouts. Não esconda uma vulnerabilidade de complexidade apenas reduzindo todos os dados.
Arbitraries grandes
Comece com tamanhos pequenos. fast-check varia tamanho e extremos. Para testes específicos de escala, use benchmark ou carga em vez de tentar cobrir performance apenas com propriedades.
Erros comuns
- assertar apenas que não lança;
- reimplementar a função no teste;
- usar muitos filtros;
- gerar dados fora do domínio;
- ignorar seed da falha;
- usar aleatoriedade manual;
- tentar substituir todos os exemplos;
- não limpar estado assíncrono.
CI
Rode propriedades rápidas em todos os PRs. Em agenda noturna, aumente numRuns. Preserve seed e path no log e evite truncar a saída que permite reprodução.
Property testing e mutation testing
fast-check explora entradas; Stryker altera implementação. Juntos, eles aumentam a chance de detectar limites e verificar se propriedades realmente protegem comportamento.
Fluxo recomendado
Escolha uma função com invariantes claros, modele arbitraries do domínio, deixe shrinking trabalhar, reproduza seeds e mantenha exemplos para regressões. Combine com Stryker no Node.js, Node Test Runner, Testcontainers e pipelines em GitHub Actions.
Consulte a documentação oficial sobre property-based testing e a API oficial do fast-check.


