O Property-Based Testing no Node.js verifica propriedades gerais do sistema usando muitos valores gerados automaticamente. Em vez de escrever apenas exemplos específicos, como “2 + 3 deve resultar em 5”, o teste descreve uma regra que precisa ser verdadeira para uma ampla variedade de entradas.
A biblioteca fast-check gera valores de forma determinística, executa a propriedade várias vezes e, quando encontra uma falha, reduz o caso até produzir um contraexemplo pequeno. Esse processo, chamado shrinking, ajuda a encontrar combinações que testes manuais dificilmente cobririam.
Neste guia, você aprenderá properties, arbitraries, shrinking, seeds, testes assíncronos, modelos, comandos, validação, datas, APIs, persistência e boas práticas com fast-check.
O que é Property-Based Testing?
Property-Based Testing descreve invariantes ou relações que devem permanecer verdadeiras para muitos valores. A documentação oficial do fast-check apresenta fc.assert, fc.property, arbitraries, geração por seed e shrinking. A introdução oficial ao conceito detalha a diferença entre exemplos e propriedades.
Para testes de integração reais, consulte Testcontainers no Node.js. Para testes nativos, veja Node Test Runner.
Instalação
npm install --save-dev fast-checkfast-check funciona com Node Test Runner, Vitest, Jest, Mocha e outros runners.
Primeira propriedade
import fc from 'fast-check';
import assert from 'node:assert/strict';
test('uma string contém a si mesma', () => {
fc.assert(
fc.property(fc.string(), text => {
assert.equal(text.includes(text), true);
})
);
});O arbitrary fc.string() produz várias strings, incluindo vazias, Unicode e combinações pouco comuns.
Propriedade versus exemplo
Um exemplo:
assert.equal(normalizeEmail(' USER@example.com '), 'user@example.com');Uma propriedade:
fc.assert(
fc.property(fc.emailAddress(), email => {
const normalized = normalizeEmail(email);
assert.equal(
normalizeEmail(normalized),
normalized
);
})
);A propriedade verifica idempotência: normalizar duas vezes produz o mesmo resultado.
Runner
fc.assert(property, {
numRuns: 1000
});Mais execuções aumentam cobertura estatística e custo. Use valores adequados à velocidade da propriedade.
Arbitraries
Arbitraries geram e reduzem valores:
fc.integer();fc.nat();fc.float();fc.string();fc.uuid();fc.emailAddress();fc.date();fc.array();fc.record();fc.option().
Limitando valores
const quantityArbitrary = fc.integer({
min: 1,
max: 1000
});Não restrinja demais apenas para o teste passar. Os limites devem representar o domínio.
Records
const orderInputArbitrary = fc.record({
customerId: fc.uuid(),
quantity: fc.integer({ min: 1, max: 20 }),
priceCents: fc.integer({ min: 0, max: 1_000_000 })
});Records geram objetos coerentes e facilitam composição.
Map
const moneyArbitrary = fc
.integer({ min: 0, max: 10_000_000 })
.map(cents => Money.create(cents, 'BRL'));O shrinking ocorre no inteiro antes da transformação.
Filter
const evenArbitrary = fc
.integer()
.filter(value => value % 2 === 0);Filtros que rejeitam muitos valores deixam geração lenta. Prefira construir diretamente:
const evenArbitrary = fc.integer().map(value => value * 2);Tuple
const rangeArbitrary = fc
.tuple(fc.date(), fc.nat({ max: 365 }))
.map(([start, days]) => ({
start,
end: new Date(
start.getTime() + days * 86_400_000
)
}));Oneof
const statusArbitrary = fc.constantFrom(
'pending',
'approved',
'cancelled'
);fc.oneof() combina arbitraries de estruturas diferentes.
Shrinking
Quando uma propriedade falha com um array enorme, fast-check tenta encontrar o menor array que ainda falha. O resultado costuma apontar a causa com mais clareza.
Contraexemplo
Uma falha informa:
- seed;
- path;
- contraexemplo;
- quantidade de execuções;
- detalhes da exceção.
Copie seed e path para reproduzir exatamente.
Reprodução
fc.assert(property, {
seed: 123456789,
path: '12:3:1'
});Registre essas informações no CI.
Seeds determinísticas
A geração parece aleatória, mas é reproduzível. Isso permite explorar muitos casos sem transformar a suíte em comportamento não determinístico.
Propriedades matemáticas
Exemplos úteis:
- comutatividade;
- associatividade;
- identidade;
- inversão;
- idempotência;
- monotonicidade.
Money
test('somar zero não altera dinheiro', () => {
fc.assert(
fc.property(
fc.integer({ min: 0, max: 1_000_000 }),
cents => {
const money = Money.create(cents, 'BRL');
const zero = Money.create(0, 'BRL');
assert.equal(
money.add(zero).equals(money),
true
);
}
)
);
});Consulte Value Objects no Node.js.
Round trip
Serialização e parsing devem preservar o valor:
fc.assert(
fc.property(orderArbitrary, order => {
const serialized = serializeOrder(order);
const parsed = parseOrder(serialized);
assert.deepEqual(parsed, order);
})
);Encode e decode
Propriedade comum:
decode(encode(value)) === valueEla encontra problemas com Unicode, campos opcionais e limites numéricos.
Ordenação
fc.assert(
fc.property(
fc.array(fc.integer()),
values => {
const sorted = sortNumbers(values);
assert.equal(sorted.length, values.length);
for (let index = 1; index < sorted.length; index += 1) {
assert.ok(sorted[index - 1] <= sorted[index]);
}
}
)
);Também verifique que os elementos foram preservados.
Não altere entrada
const original = structuredClone(values);
sortNumbers(values);
assert.deepEqual(values, original);Use quando o contrato promete imutabilidade.
Datas
Datas geradas podem incluir valores extremos. Restrinja ao período válido do domínio e cuide de datas inválidas.
fc.date({
min: new Date('2000-01-01T00:00:00Z'),
max: new Date('2100-01-01T00:00:00Z')
});Timezone
Teste conversões em UTC e limites de dia. Alterar timezone do processo em diferentes jobs de CI pode encontrar suposições locais.
Strings Unicode
Gere espaços, caracteres compostos, emoji e normalização. APIs que usam length precisam decidir se contam code units, code points ou graphemes.
Validação
Uma propriedade para parser:
fc.assert(
fc.property(fc.anything(), value => {
const result = schema.safeParse(value);
assert.equal(
result.success === true || result.success === false,
true
);
})
);O parser não deve lançar para entrada arbitrária se o contrato retorna Result.
Result Pattern
Teste que toda entrada produz Ok ou Err válido. Consulte Result Pattern no Node.js.
Testes assíncronos
await fc.assert(
fc.asyncProperty(
orderInputArbitrary,
async input => {
const result = await service.execute(input);
assert.ok(result.id);
}
),
{ numRuns: 50 }
);Propriedades com banco são mais lentas. Reduza runs e mantenha isolamento.
Condições prévias
fc.pre(divisor !== 0);Preconditions descartam casos. Muitos descartes indicam arbitrary mal desenhado.
Classificação
Classifique tipos de entrada para entender cobertura:
fc.classify(items.length === 0, 'empty');
fc.classify(items.length > 100, 'large');Use as APIs disponíveis na versão instalada.
Model-Based Testing
fast-check pode gerar sequências de comandos e comparar sistema real com um modelo simplificado.
type Model = {
balance: number;
};
type Real = {
account: AccountService;
};Comandos implementam:
- check;
- run;
- toString.
Exemplo de conta
Comandos de deposit, withdraw e reset são gerados em ordens variadas. O modelo calcula saldo esperado e o sistema real precisa acompanhar.
Máquinas de estado
Model-based testing é útil para:
- carrinho;
- workflow;
- cache;
- autenticação;
- locks;
- filas;
- protocolos.
Concorrência
Property-based testing pode gerar operações concorrentes, mas reproduzir scheduling é difícil. Controle barreiras e registre seeds.
APIs HTTP
Gere payloads válidos e inválidos. Confirme:
- nunca retorna stack;
- status corresponde ao contrato;
- campos desconhecidos são rejeitados;
- limites são respeitados;
- serialização é válida.
OpenAPI
Schemas OpenAPI podem gerar dados por ferramentas do ecossistema. Combine com fast-check para invariantes específicas.
Banco real
Use Testcontainers para property tests de repository com poucas execuções. Limpe dados em cada run ou gere IDs únicos.
Constraints
Gere concorrência e valores de limite para verificar unique, checks, lock otimista e transações.
Não substitui exemplos
Testes baseados em exemplos são melhores para:
- cenários de negócio nomeados;
- mensagens específicas;
- regressões conhecidas;
- documentação;
- casos obrigatórios.
Use os dois estilos.
Casos de regressão
Quando fast-check encontra um bug, mantenha a propriedade e considere adicionar o contraexemplo como teste explícito.
Arbitraries de domínio
Crie uma biblioteca interna:
export const customerArbitrary = fc.record({
id: fc.uuid(),
email: fc.emailAddress(),
plan: fc.constantFrom('free', 'premium')
});Isso mantém regras consistentes entre suítes.
Dados inválidos
Também crie arbitraries deliberadamente inválidos, como quantidade zero, IDs malformados e strings acima do limite.
Distribuição
Geração uniforme pode não representar produção. Use pesos e categorias para incluir muitos limites e combinações críticas.
Performance
Uma propriedade lenta executada mil vezes bloqueia o CI. Separe:
- rápidas com muitos runs;
- integração com poucos runs;
- nightly com exploração maior.
Timeout do runner
Ajuste timeout por propriedade e evite operações sem cancelamento.
Logs em falha
Não registre cada valor gerado. fast-check já fornece contraexemplo. Capture contexto adicional apenas quando necessário.
CI
Em falha, preserve:
- seed;
- path;
- versão do fast-check;
- Node.js;
- timezone;
- contraexemplo;
- commit.
Seeds fixas?
Uma seed global fixa torna a suíte repetível, mas reduz exploração entre execuções. Uma estratégia é usar seed variável e confiar no relatório para reprodução.
Nightly
Execute mais runs em pipeline noturno e mantenha um número menor em pull requests.
Erros comuns
- Propriedade tautológica: o teste repete a implementação.
- Filter excessivo: geração fica lenta.
- Limites artificiais: bugs são escondidos.
- Sem seed no log: falha não reproduz.
- Property gigante: causa fica confusa.
- Banco compartilhado: runs interferem.
- Somente property tests: regras nomeadas perdem clareza.
Boas práticas
- Descreva invariantes.
- Use arbitraries de domínio.
- Aproveite shrinking.
- Registre seed e path.
- Evite filtros caros.
- Teste round trips.
- Inclua limites.
- Combine com exemplos.
- Controle custo assíncrono.
- Mantenha contraexemplos importantes.
Conclusão
O Property-Based Testing no Node.js amplia a cobertura ao gerar muitos valores e procurar contraexemplos automaticamente. fast-check combina arbitraries, propriedades e shrinking de forma reproduzível.
O maior valor aparece em invariantes, parsers, Value Objects, algoritmos e máquinas de estado. Com seeds registradas, arbitraries bem desenhados e integração equilibrada com testes por exemplo, a técnica encontra bugs sem tornar a suíte imprevisível.



