Como uso milhares de testes gerados para detectar bugs ocultos em meu código

por Nada Em Troca
7 minutos de leitura
Como uso milhares de testes gerados para detectar bugs ocultos em meu código

Você já viu “NaN” aparecer em um site mal codificado? Ou ficou perplexo com um bug persistente, apenas para descobrir que há alguma peculiaridade na linguagem? O problema está parcialmente relacionado às suas suposições e provavelmente a um conjunto de testes incompleto. Explicarei o que é teste baseado em propriedades (PBT) e como ele resolve esses problemas.

O que são testes baseados em propriedades (PBT)?

Em um nível muito alto, ele injeta milhares de valores aleatórios em testes — criando milhares de testes no processo. O PBT contrasta os testes baseados em exemplos, que comparam os resultados com alguns valores considerados bons.

Vejamos “testes baseados em exemplos” usando JavaScript:

// Node.js test("adds two numbers", () => {   assert(add(1, 2) == 3); });

A abordagem ingênua é assumir que este caso de teste cobre tudo. Mas, para sermos realmente completos, também poderíamos testar números negativos, zero, pontos flutuantes, os menores e maiores números inteiros seguros (Number.MIN_SAFE_INTEGER e Number.MAX_SAFE_INTEGER) e valores fora desse intervalo. Para um código robusto, poderíamos até mesmo lançar nele todos os tipos de dados concebíveis e verificar se ele os trata normalmente, lançando um erro. Um fardo demorado.

Depois de escrever dezenas de testes, você poderá desenvolver algo assim:

function add(a, b) {   if (typeof a !== "number") throw Error();   if (typeof b !== "number") throw Error();   return a + b; }

Isso parece razoável, certo? Exceto:

typeof NaN === "number"; // -> true

Então isso iria passar despercebido. Vamos melhorar isso.

function add(a, b) {   if (typeof a !== "number") throw Error();   if (typeof b !== "number") throw Error();   if (a === NaN) throw Error();   if (b === NaN) throw Error();   return a + b; }

Certamente agora…

add(NaN, 1);
O texto em uma janela de terminal mostra a saída da função add – é simplesmente NaN.

Eu, por exemplo, estou chocado. Se você não sabia, NaN não é igual a si mesmo, então “a === NaN” é sempre falso. Essas coisas são ocorrências comuns em JavaScript porque é uma linguagem mal pensada. No entanto, foram nossas suposições que nos colocaram em apuros. É difícil imaginar todos os casos extremos e, se esse código fosse voltado ao público, quem sabe que danos ele poderia causar?

Os testes baseados em propriedades jogam tudo e a pia da cozinha em seu código. Em vez de alguns exemplos, gera milhares e cobre todo o domínio de entrada. Ele expõe muitas suposições e garante que seu código seja robusto.

Como funciona o PBT?

Existem três elementos principais para o PBT:

  • Propriedades: Aspectos desejáveis ​​ou indesejáveis ​​do seu sistema – aquilo que você está testando.
  • Geradores: Funções responsáveis ​​por gerar grandes quantidades de parâmetros.
  • Redução: O que a biblioteca PBT fará com os parâmetros com falha que encontrar (abordados posteriormente).

Propriedades

Pesquisar PBT na internet leva você rapidamente a um mundo de jargão acadêmico. As propriedades vêm em várias formas e muitas não são adequadas para iniciantes (nem para consumo humano). Aqui estão alguns exemplos simples e comuns:

  • Idempotência: a qualidade de uma operação que produz o mesmo resultado quando executada várias vezes.
  • Invariantes: coisas que são sempre verdadeiras – como a água está molhada.
  • Restrições de domínio: intervalos de valores válidos/inválidos.

Estes são para citar alguns. Existem mais, mas essa é a ideia. Alguns são difíceis de entender, então tome cuidado.

O PBT coloca muita ênfase no teste de “propriedades”, quando, na realidade, até mesmo os testes baseados em exemplos têm como alvo as propriedades do sistema. Não pense demais nesta parte. Trate o PBT como testes de unidade generativos. Você pode aprender sobre as propriedades à medida que avança.

Geradores

As bibliotecas PBT vêm com uma API extensa para gerar qualquer valor concebível. Eles são compostos de funções geradoras combináveis, flexíveis o suficiente para serem combinadas de qualquer forma, produzindo qualquer estrutura de dados complexa que você desejar.

Encolhendo

Os dados gerados podem ser complexos e difíceis de entender. Para melhorar a compreensão, o PBT usa a redução. Encolher é um processo simples. Quando encontra um valor com falha, ele é repetidamente reduzido e testado novamente. Este processo continua até encontrar o menor valor com falha. Por exemplo, reduzir um número de bilhões para 0 ou reduzir uma lista enorme a uma lista vazia. O processo de redução encontra o caso de falha mais simples, o que o torna mais razoável.

Qual é a aparência do PBT?

Desde que comecei com JavaScript, continuarei com ele – fast-check é uma biblioteca PBT brilhante e a usarei nos exemplos a seguir.

Vamos começar com uma função básica de “adicionar” e melhorá-la gradativamente.

function add(a, b) {   return a + b; }

Agora vamos criar nosso arquivo de teste:

const fc = require("fast-check"); const { add } = require("./add");  test("adds two numbers", () => {   fc.assert(     fc.property(fc.integer(), fc.integer(), (a, b) => {       const result = add(a, b);       return result === a + b;     }),   ); });
O texto em uma janela do terminal indica que um teste foi aprovado.
  • “fc.assert”: executa vários testes de propriedades e reduz casos de falha.
  • “fc.property”: Seus argumentos (geradores) descrevem a propriedade que você deseja testar.
  • “fc.integer”: Um gerador que produz um número inteiro aleatório.

A forma geral da função “fc.property” é:

// "fc.assert()" runs this function many times. // Each generator produces one random value per execution. fc.property(   ...arbitraries // Aka generators. Functions that produce a random value.   (...args) => {       // A predicate function. Return true to pass and false to fail.       // You can alternatively use expect() here.       // The generators inject a random value into each "arg."   }; );

Vamos agora lançar dados (objetos) inesperados em nossa função e ver se isso gera um erro.

test("throws for objects", () => {   fc.assert(     fc.property(fc.integer(), fc.object(), (a, b) => {       expect(() => add(a, b)).toThrow();       return true; // Otherwise we return undefined, which is falsy.     }),   ); });
O texto em uma janela de terminal indica que um teste foi aprovado, mas outro falhou. As anotações na tela mostram que o parâmetro com falha era um objeto.

Esperávamos que ocorresse um erro ao receber objetos, mas isso não aconteceu. Vamos atualizar a função.

function add(a, b) {   if (typeof a !== "number") throw Error();   if (typeof b !== "number") throw Error();   return a + b; }
O texto em uma janela de terminal indica que todos os dois testes foram aprovados.

Porém, vamos jogar tudo nisso, inclusive a pia da cozinha (NaN).

// A simple function that returns our generator. function kitchenSink() {   // The generated value will be one of NaN or anything (except a number).   return fc.oneof(     fc.constant(NaN),     // Everything except numbers.     fc.anything().filter((t) => typeof t !== "number"),   ); }  test("everything and the kitchen sink", () => {   fc.assert(     fc.property(kitchenSink(), kitchenSink(), (a, b) => {       expect(() => add(a, b)).toThrow();       return true;     }),   ); });
O texto em uma janela de terminal indica que dois testes foram aprovados, mas um falhou. Existem anotações na tela que indicam que após 5 testes, um valor NaN causou falha.

Nossa função não gera um erro para NaN, porque NaN é na verdade um número. Este é o tipo de suposição que normalmente nos surpreende. Vamos resolver o problema e testá-lo.

function add(a, b) {   if (typeof a !== "number") throw Error();   if (typeof b !== "number") throw Error();   if (isNaN(a)) throw Error();   if (isNaN(b)) throw Error();   return a + b; }
O texto em uma janela do terminal indica que todos os três testes foram aprovados.

Agora, mais um teste para garantir que nossa função retorne um número válido ou gere um erro.

test("always returns a number or throws", () => {   fc.assert(     fc.property(fc.anything(), fc.anything(), (a, b) => {       try {         // Number.isFinite() returns false for all non-numbers.         // Returning false fails the test.         // Therefore, this test fails if the "add" returns a non-number.         return Number.isFinite(add(a, b));       } catch (e) {         // We expect "add" to throw an error; it's receiving garbage.         return true;       }     }),     // I've set 10,000 tests (up from the default 100).     // More tests mean more chance of catching subtle bugs.     { numRuns: 10000 },   ); });
O texto em uma janela de terminal indica que três testes foram aprovados, mas um falhou. Existem anotações na tela que indicam que após 2.000 testes, um valor infinito causou sua falha.

Isso é uma coisa boa. Isso significa que estamos detectando bugs agora, em vez de preencher nossas páginas da web com NaN. Uma última mudança.

Como o PBT lança dados aleatórios em seu código, ele não revela problemas a cada execução de teste. A descoberta de bugs pode ser esporádica e aumentar o número de execuções de testes testa seu código de forma mais completa.

function add(a, b) {   if (!Number.isFinite(a)) throw new Error();   if (!Number.isFinite(b)) throw new Error();   return a + b; }
O texto em uma janela do terminal indica que todos os quatro testes foram aprovados.

Excelente. Possivelmente o código com maior engenharia da história, mas agora tenho confiança de que ele não falhará silenciosamente quando for lançado em uma curva. Ele falha exatamente onde deveria.

Interface IDE exibindo algum código JavaScript.
6 trechos de JavaScript para aprimorar seu site

Ganhos rápidos e fáceis para qualquer site que você esteja construindo.

6
Por Bobby Jack

Se você for sensato, esperaria que um código sensato funcionasse, mas o JavaScript é confuso. Até mesmo o Python pode ter peculiaridades inesperadas. Além disso, linguagens fortemente tipadas podem eliminar uma classe inteira de bugs, mas não erros de programação, bugs na especificação ou suposições tolas. Se você deseja um código robusto, precisa expô-lo a testes extensivos, e testes baseados em exemplos não são suficientes.

Python fornece uma ótima biblioteca PBT chamada Hypothesis, e Golang fornece Rapid. Se você estiver escrevendo código voltado ao público ou usando linguagens duvidosas, recomendo fortemente o PBT.

Uma mulher codificando em seu computador com símbolos de menor e maior que ao seu redor.
Torne-se um programador melhor: 7 hábitos para crescer

Hábitos testados em batalha para escrever programas melhores.

10
Por Zunaid Ali
Este artigo foi útil?
Gostei0Não Gostei0

Deixe um comentário

Are you sure want to unlock this post?
Unlock left : 0
Are you sure want to cancel subscription?
-
00:00
00:00
Update Required Flash plugin
-
00:00
00:00