Criando testes unitários com JS puro!
Published: 2023-02-10 20:07:46

## Aos apressados TL;DR

[Neste repositório](https://github.com/faakit/Simple-JS-Tester) 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](https://github.com/faakit/Simple-JS-Tester/blob/main/screenshot.png?raw=true)

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

```javascript
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:

```javascript
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`.

```javascript
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:

```javascript
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:

```javascript
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
```javascript
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
```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](https://raw.githubusercontent.com/faakit/Simple-JS-Tester/main/console-preview.png)

**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`

```html
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> 
```

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

```javascript
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.