Desenvolvimento

17 mai, 2018

Utilizando AVA para testar seu código

Publicidade

Testes unitários são o bicho de sete cabeças de todos os programadores. Tanto pela quantidade de artigos falando sobre eles (como meu próprio), quanto pela quantidade de ferramentas que temos à nossa disposição para realizar tais testes.

Para vocês terem uma noção, somente no mundo JavaScript – que será meu principal foco aqui -, temos algumas bibliotecas mais conhecidas:

Dentre muito outros que aparecem ao longo de pesquisas simples no Google. Todos tem suas vantagens e desvantagens, mas a grande maioria deles possui algo em comum: eles não possuem uma biblioteca de asserção padrão.

Mas o que raios é uma biblioteca de asserção?

Asserção de código

Quando você roda um teste, seja ele unitário ou não, basicamente você está dizendo o seguinte: “Olha, computador, estou querendo saber se este valor é igual a este outro aqui”, e o computador te dá uma resposta que, geralmente, gira em torno de um true ou um false; isso é o que chamamos de asserção.

Esse termo vem do inglês assert, que significa justamente isso. Se formos traduzir, teremos essa definição pelo dicionário:

“Proposição afirmativa ou negativa de sentido completo e intenção declarativa, que pode ser verdadeira ou falsa”.

Mas por que estamos falando disso quando queremos falar sobre testes? Simples, porque a maioria dos test runners que listei acima não utilizam a biblioteca de asserção nativa do Node. Essa biblioteca pode ser encontrada no pacote assert desde a versão 0.10 do runtime, mas – ao longo do tempo – outras bibliotecas como o Chai foram criadas.

O principal motivo para essa divisão é que a sintaxe padrão do assert não é “legível” como um texto. E, de fato, se formos comparar o Chai com o Assert do Node, vamos ver uma diferença:

const { expect } = require('chai')
const a = 2

expect(a).to.equal(2) // true
expect(a).to.be.a('string') // AssertionError: Expected 2 to be a string

Agora vejamos a mesma coisa no Assert:

const assert = require('assert')
const a = 2

assert.deepEqual(a, 2) // true
assert.ok(typeof a, 'string') // AssertionError: 'number' == 'string'

Veja que o Chai preza mais por uma sintaxe próxima da linguagem natural, enquanto o Assert é mais voltado para uma programação estruturada.

O ponto é: Precisamos mesmo de mais uma dependência só para isso? Quem já assistiu a algumas de minhas talks sobre performance e/ou JavaScript, sabe que eu não sou um grande fã da inclusão de bibliotecas inúteis no package.json somente para melhorar a legibilidade de algum código, e é ai que o AVA entra.

AVA, simples e direto

AVA é mais um test runner de JavaScript (isso mesmo, outro para a lista), porém, nele temos uma diferença: o AVA não possui uma biblioteca de asserção nativa, ou seja, ele somente roda seus testes, e ele faz isso muito bem e muito rápido, pois todas as execuções são paralelas e não compartilham contexto, então você não precisa se preocupar em criar um ambiente completo de testes, pois um teste não vai interferir no outro de maneira nenhuma.

Nosso objeto de teste

Para começar, vamos criar um código simples, um código matemático, uma soma:

function sum (...numeros) {
    return numeros.reduce((acumulador, numero) => acumulador + numero, 0)
}

Aqui estamos basicamente recebendo um parâmetro rest, que vai ser uma sequência de números (não vamos validar se são mesmo números) e vamos retornar a soma de todos eles, portanto, sum(2,2,2) nos daria 6.

Vamos colocar isso em um arquivo functions.js:

function sum (...numbers) {
  return numbers.reduce((acc, number) => acc + number, 0)
}

module.exports = { sum }

Testando

Para começarmos o teste, vamos criar uma nova pasta e executar npm install ava —save-dev para instalar o AVA como dependência. Em seguida vamos criar uma pasta tests. Por padrão, o AVA busca uma pasta com esse nome, ou então arquivos terminados em *.test.js, e para finalizar a configuração, vamos no nosso package.json e vamos adicionar um script de teste:

{
  "name": "artigo-ava",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "test": "ava"
  },
  "devDependencies": {
    "ava": "^0.25.0"
  }
}

Estamos prontos para começar. Dentro da nossa pasta tests, vamos criar um arquivo sum.test.js e vamos começar a escrever nosso código de teste. A primeira coisa que fazemos é importar o AVA e nosso arquivo de funções (que queremos testar) no topo do arquivo:

import describe from 'ava'
import {sum} from '../functions'

Aqui, veja três coisas:

  1. Eu não chamei o ava de ava, mas sim de describe para podermos seguir a nomenclatura padrão dos testes
  2. Sim, estamos usando a sintaxe import porque o AVA nos permite escrever testes nessa sintaxe sem nos preocupar em transpilação
  3. Não precisamos importar o assert do Node, porque o AVA automaticamente já injeta essa dependência por nós

Você irá perceber que o Assert do AVA é um pouco diferente da API padrão do Assert do Node, isso porque o AVA criou um wrapper em torno dela para que algumas funções fiquem um pouco mais simples de utilizar.

Agora vamos escrever o primeiro caso de testes, o caso de sucesso. Passaremos três elementos e compararemos a soma da nossa função com o valor final:

describe('Deve retornar 6', (assert) => {
  const resultado = 6
  assert.deepEqual(sum(2, 2, 2), resultado)
})

Viu porque coloquei describe ao invés de ava? A função de teste recebe um parâmetro que é o contexto do teste. Podemos dar qualquer nome; resolvi chamar de assert para ficar mais próximo ao que já usamos, mas esse parâmetro é um objeto global enviado para cada teste como um objeto vazio. Dessa forma, se precisarmos, por exemplo, compartilhar o resultado da nossa função soma com outros testes, podemos fazer o seguinte:

import describe from 'ava'
import {sum} from '../functions'

describe.beforeEach((test) => {
  test.context = { resultado: 6 }
})

describe('Deve retornar 6', (assert) => {
  assert.deepEqual(sum(2, 2, 2), assert.context.resultado)
})

Veja que estou setando um objeto que será meu contexto apenas para este teste, os arquivos de teste não compartilham contextos. Vamos escrever agora um caso de erro:

describe('Não deve retornar 6', (assert) => {
  assert.notDeepEqual(sum(2, 2, 2, 2), assert.context.resultado)
})

E se passarmos uma string, alteraremos a nossa função:

function sum (...numbers) {
  if (numbers.filter((number) => typeof number !== 'number').length > 0) throw new Error('Esperamos apenas números')
  return numbers.reduce((acc, number) => acc + number, 0)
}

Basicamente filtramos o array de argumentos procurando por algo que não seja um número. Se existir, então jogamos um erro. Vamos capturar esse erro no nosso teste:

describe('Deve retornar um erro', (assert) => {
  assert.throws(() => sum('a', 'b', 'c'), Error)
})

Podemos fazer dessa forma, também:

describe('Deve retornar um erro', (assert) => {
  const error = assert.throws(() => sum('a', 'b', 'c'))
  assert.deepEqual(error.message, 'Esperamos apenas números')
})

E podemos – claro – misturar os dois, assim validamos o tipo e a mensagem:

describe('Deve retornar um erro', (assert) => {
  const error = assert.throws(() => sum('a', 'b', 'c'), Error)
  assert.deepEqual(error.message, 'Esperamos apenas números')
})

Coverage

Code Coverage é o termo utilizado para ferramentas que analisam o código linha a linha, verificando se, de fato, você testou todas as possibilidades. A mais famosa dentre elas é o Istanbul, porque ele permite a adição de coverage sem que sejam feitas muitas configurações adicionais.

O Istanbul tem uma linha de comando chamada NYC que foi feita basicamente para integrações entre ferramentas, e é claro que o AVA não ficou de fora. O suporte ao NYC é nativo, basta adicionarmos a dependência com npm install nyc —save-dev e criarmos um novo script chamado coverage dessa forma:

{
  "name": "artigo-ava",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "test": "ava",
    "coverage": "nyc npm test"
  },
  "devDependencies": {
    "ava": "^0.25.0",
    "nyc": "^11.7.3"
  }
}

O que estamos falando aqui é para o NYC instrumentar nosso runtime retornado pelo npm test. Poderíamos colocar nyc ava que também funcionaria, e teremos um output deste tipo:

Conclusão

Agora ficou simples e rápido começar seus testes unitários com JavaScript, lembrando que o AVA é uma de muitas ferramentas excelentes no mercado. Veja as diferenças entre elas; qual se adequa melhor ao seu uso, e também com qual delas você se acostuma mais rápido. Afinal, é muito mais simples usar algo que você está entendendo do que um monte de código complicado, não é mesmo?

Até a próxima!