Home > Blog > Desenvolvimento Web
Desenvolvimento Web
JavaScript
Programação

Property-Based Testing no Node.js

Atualizado em: 6 de setembro de 2026

Estação de trabalho usada no desenvolvimento de aplicações Node.js

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

fast-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)) === value

Ela 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.

10 melhores cursos de programação em 2026

Descubra os melhores cursos de programação. Aprenda a escolher o curso ideal para iniciar ou avançar na carreira de desenvolvedor

POSTS RELACIONADOS

Ver todos

Seta para a direita