Executando verificação de segurança...
1

Criando testes unitários com JS puro!

Aos apressados TL;DR

Neste repositório há um exemplo simplório da implementação de testes unitários com inspiração no Jest, com direito à visualização no browser.

DIY JS Tester

preview

Motivação

Recentemente fiz um desafio em JS, e sempre imaginei que lidar com a segurança dos testes é algo muito importante, tanto ao se fazer código quanto ao realizar manutenções no mesmo, porém, como poderia fazê-los sem o auxílio do fiel companheiro Jest? 😯 Logo resolvi pegar o desafio de implementar uma forma simples mas que pudesse me trazer todos os recursos que eu precisava naquele momento, com isso pude aprender ainda mais sobre a linguagem e seus recursos!

Mão na massa!

Primeiro de tudo, iniciaremos um arquivo para receber o código do testador, assim poderemos deixar as coisas o mais "separadas" o possível.

tester.js

Aqui, iniciaremos o nosso código com a descrição dos testes

const describe = (module, fn) => {
    // printModule(module);
    console.info(`---- ${module} ----`);
    fn();
    console.info('\n\n');
}

const it = (desc, fn) => {
    try {
        fn();
        // printTest(desc);
        console.log(`PASS - ${desc}`);
    } catch (error) {
        // printTest(desc, error);
        console.log(`FAIL - ${desc}`);
        console.error(error);
    }
}

Falaremos das funções de prefixo print posteriormente. Primeiro, criamos uma estrutura de funções que nos permite escrever testes separados em "módulos" que poderão ser escritos da seguinte forma:

describe("funcao imprimeLinha", () => {
    it("deve imprimir a linha", () => {
        ...
    })

    it("não deve pular linha", () => {
        ...
    })
})

Agora, bastar criar uma função de asserção, e voilá, seus testes já podem dar os primeiros passos!

Mas para isso, faremos de uma forma que nos permite expandir e ir além.

Nossa primeira asserção será de igualdade, para isso, criaremos uma classe _assert, que sempre será chamada por uma função assert, fazendo que não seja necessário a utilização do operador de instanciação de classe new.

class _assert {
    constructor(value) {
        this.value = value;
    }
    toBe(expected) {
        if (!Object.is(this.value, expected)) {
            throw new Error(`"\nEsperado: ${JSON.stringify(expected)} \nRecebido: ${JSON.stringify(this.value)}\n`);
        }
    }

const assert = (value) => new _assert(value);

A nossa classe _assert recebe um valor em seu construtor que poderá ser qualquer coisa, assim, ao chamar seu método toBe(expected), o mesmo irá comparar com o auxílio da função Object.is() se o valor recebido (value) é o mesmo que o esperado (expected).

Assim, ao criar nossos testes, poderemos fazer uma asserção da seguinte forma:

assert(criaString('ola mundo')).toBe('ola mundo');

Porém, como estamos lidando com Javascript, você deve saber (ou não, e está aprendendo agora) que 0.1 + 0.2 não é igual a 0.3 (é 0.30000000000000004), tal como um array [] nunca é igual a outro array ([] === [] -> false), para isso, estaremos criando mais duas asserções, e aproveitando que isso é uma classe, criaremos também um modificador not(), que ira fazer justamente o contrário, e esperar que o valor seja diferente de um esperado, assim nossa classe terminará dessa forma:

class _assert {
    isNot = false;
    constructor(value) {
        this.value = value;
    }
    not() {
        this.isNot = true;
        return this;
    }
    toBe(expected) {
        if(this.isNot && Object.is(this.value, expected)){
            throw new Error(`"\nNão esperado: ${JSON.stringify(expected)} \nRecebido: ${JSON.stringify(this.value)}\n`);
        }

        if (!Object.is(this.value, expected) && !this.isNot) {
            throw new Error(`"\nEsperado: ${JSON.stringify(expected)} \nRecebido: ${JSON.stringify(this.value)}\n`);
        }
    }

    toBeCloseTo(expected, precision = 2) {
        const roundedExpected = Number(expected.toFixed(precision));
        const roundedValue = Number(this.value.toFixed(precision));

        if(this.isNot && Object.is(roundedValue, roundedExpected)){
            throw new Error(`"\nNão esperado (aproximado): ${JSON.stringify(roundedExpected)} \nRecebido: ${JSON.stringify(roundedValue)}\n`);
        }

        if (!Object.is(roundedValue, roundedExpected) && !this.isNot) {
            throw new Error(`"\nEsperado: ${JSON.stringify(expected)} \nRecebido: ${JSON.stringify(this.value)}\n`);
        }
    }

    toEqual(expected) {
        if(this.isNot && JSON.stringify(this.value) === JSON.stringify(expected)){
            throw new Error(`"\nNão esperado: ${JSON.stringify(expected)} \nRecebido: ${JSON.stringify(this.value)}\n`);
        }
        
        if (JSON.stringify(this.value) !== JSON.stringify(expected) && !this.isNot) {
            throw new Error(`"\nEsperado: ${JSON.stringify(expected)} \nRecebido: ${JSON.stringify(this.value)}\n`);
        }
    }
}

Para que possamos testar com floats que poderão nos trazer algum número com inúmeras casas decimais, criamos a função toBeCloseTo(expected, precision = 2), ela receberá o número esperado e a precisão (com a padrão sendo 2 casas decimais quando não especificado), assim o número será arredondado antes de ser comparado!

E para que possamos testar arrays e outros objetos que poderiam trazer alguma confusão criamos também a função toEqual(expected), a estratégia aqui é transformar nossos valores com o JSON.strigify() para sua forma em JSON string, e então compará-los.

Para criação do modificador colocamos mais uma propriedade em nossa classe, isNot, que nos dirá quando queremos que o valor seja diferente, para isso, alteramos em todas as funções o que fazer quando esse modificador for verdadeiro.

Agora basta criar o nosso index.html e inserir nosso testador e um arquivo de testes como esse:

simple.test.js

describe('simple tests', () => {
    it('should be true', () => {
        assert(true).toBe(true);
    });

    it('should be equal arrays', () => {
        const array = [1, 2, 3];
        const array2 = [1, 2, 3];

        assert(array).toEqual(array2);
    });

    it('0.1 + 0.2 should be 0.3', () => {
        assert(0.1 + 0.2).toBeCloseTo(0.3);
    });

    it('should fail', () => {
        assert(0.1 + 0.2).toBe(0.3);        
    });
})

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Test Runner</title>
</head>

<body>
  <div id="root" class="d-grid p-3 gap-3">
    
  <script src="tester.js"></script>
  <script src="simple.test.js"></script>
</body>

</html>

A nossa div root será utilizada logo mais 😉

E ao abrir o terminal, será possível ver isso no console:
Testes no console

Mas André, estou enjoado do console! E aquela interface bonitinha do início do artigo?!

O que vocês verão abaixo pode ser um crime de código, então, eu estou super disposto a receber dicas de melhorias, para entender como poderia ter deixado as coisas menos macarrônicas por aqui!

Primeiramente, inclua a biblioteca do bootstrap no arquivo index.html

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> 

retornaremos no arquivo tester.js e incluiremos o seguinte código:

const root = document.getElementById('root');

const printTest = (desc, error) => {
    const card = document.createElement('div');
    card.className = `card ${!error ? 'bg-success' : 'bg-danger'}`;
    const cardBody = card.appendChild(document.createElement('div'));
    cardBody.className = 'card-body';
    const header = cardBody.appendChild(document.createElement('div'));
    header.className = 'd-inline';
    const h5 = header.appendChild(document.createElement('h5'));
    h5.className = 'me-2 d-inline';
    h5.innerText = !error ? 'PASS -' : 'FAIL -';
    const h5_2 = header.appendChild(document.createElement('h5'));
    h5_2.innerText = desc;
    h5_2.className = 'd-inline';
    document.getElementById(moduleName).appendChild(card);
    if (error) {
        const errorElement = cardBody.appendChild(document.createElement('div'));
        errorElement.className = 'card bg-light mt-3';
        const pre = errorElement.appendChild(document.createElement('pre'));
        const code = pre.appendChild(document.createElement('code'));
        code.innerText = error.stack;
    }
}

let moduleName = '';
const printModule = (module) => {
    const moduleElement = root.appendChild(document.createElement('div'));
    moduleElement.id = module;
    moduleElement.className = 'd-grid gap-2'
    moduleName = module;
    const h3 = moduleElement.appendChild(document.createElement('h3'));
    h3.innerText = module;
}

E você já pode descomentar a parte do código referente às funções com prefixo print.

O que fazemos primeiramente é encontrar a nossa div root para que possamos criar novos elementos na DOM, com isso, criamos uma variável global chamada moduleName que servirá para imprimir os testes de um módulo ali dentro.

A função printModule(module) simplesmente recebe o nome do módulo de teste e imprime um Header3 na tela com esse nome e uma Div que será utilizada para mostrar os testes com um distanciamento que mantém o conforto visual!

Já a função printTest(desc, error) receberá a descrição do teste, e caso exista um erro, ele também será passado. A função criará um card com a descrição e utilizará do argumento error para definir a cor do card e se ele deverá mostrar o rastreamento de pilha, necessário para entender de onde vem o erro.

Carregando publicação patrocinada...