Gabriel / Ramos

⟨ pintor de pixel ⟩

  • Blog
  • Anatomia de um teste em JavaScript

Anatomia de um teste em JavaScript

Como funcionam testes comportamentais e que estruturas as ferramentas utilizam para otimizar sua escrita e manutenção

7 min. de leitura

Foto por Nino Liverani

Se você trabalha utilizando ou já leu algo sobre testes, talvez já tenha se deparado com algumas estruturas, termos e nomenclaturas que podem causar algumas confusões.

É comum tentar assemelhar seus testes à maneira que seu software é utilizado pelos seus usuários. Isso é uma parte muito importante do processo de confiança que você cria com o código que você desenvolve. Um pouco na linha de pensamento utilizada no BDD (Behavior Driven Development ou Desenvolvimento Guiado a Comportamento), embora seja um tópico um pouco mais abrangente.

Entender como funciona a anatomia de um teste, sabendo os pedaços envolvidos no processo e como seu teste é avaliado é parte importante nesse processo todo.

Para prosseguirmos com alguns exemplos de código, seguirei algumas estruturas propostas por frameworks pelo Jest, acredito que é uma das ferramentas que tem mais visibilidade hoje em dia. Mas existem outras no mercado que possuem estruturas semelhantes e, os fundamentos, podem ser aproveitados da mesma maneira.

Cobertura (ou coverage)

É um dos termos mais comuns e acho que é o que mais utilizamos como parâmetro para algumas decisões e resultados quando estamos escrevendo testes.

A cobertura de testes indica quais partes de seu código estão sendo executadas. Por exemplo: se temos um código de 10 linhas e uma cobertura de 50%, isso quer dizer que apenas metade das linhas desse código (ou seja, 5 linhas) estão sendo executadas ao longo dos testes.

Algumas ferramentas geram alguns relatórios visuais em HTML ou até mesmo diretamente no seu terminal, como é o caso do Jest, que oferece um report com uma tabela parecida com essa:

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 data.js  |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|

Vale a pena darmos uma passada sobre cada uma das categorias desse relatório de cobertura.

  • Arquivo (ou file): nessa coluna podemos ver todos os arquivos que foram executados ao longo dos testes escritos;
  • Declarações (ou statements/stmts): indica quais os termos de declaração (como variáveis e imports) foram ou não cobertos ao executar os testes;
  • Ramificações (ou branch): analisando a palavra branch temos ramo (ou "galho") como tradução em português o que faz com que essa coluna possa ser confundida com as branches do git, então vale ressaltar que são coisas diferentes. Os valores que aparecem nessa coluna não condizem com a cobertura de sua branch do git, mas sim com as ramificações de seu código. Podemos entender como ramificação qualquer trecho de código que divide a execução de nosso programas em duas ou mais partes, ou seja: trechos com blocos if/else, switch/case ou ternários; O que essa coluna relata é justamente essa questão: quantas ramificações (trechos de execuções distintas) do código escrito nos arquivos foram executadas.
  • Funções (ou functions/funcs): indica se as funções (rotinas e sub-rotinas) do seu código foram executadas ao longo dos seus testes;
  • Linhas (ou lines): indica quantas linhas do código estão cobertas;
  • Linhas não cobertas (ou uncovered lines): ao contrário do item anterior, indica quais linhas não foram cobertas nos testes.

Agora vamos ver alguns trechos e exemplos mais práticos e entender como eles funcionam.

Suítes de teste (ou test suites)

Suítes de teste é um termo utilizado pra exemplificar um agrupamento de testes. Basicamente! :)

É comum que, no caso do Jest, isso seja confundido com os blocos de describe, que são usados para agrupar alguns testes dentro de um mesmo arquivo, mas não é o caso. Pode parecer confuso, mas o Jest considera que cada arquivo é uma suite de teste diferente.

Nesse caso, a utilização do describe é exclusivamente para realizar um agrupamento mais específico de testes relacionados, por exemplo:

describe('Calculadora', () => {
  // testes relacionados aos cálculos
});

Essa estrutura é totalmente opcional, já que é possível criar testes utilizando outras funções que veremos a seguir. Entretanto, é uma boa ferramenta para aprimorar a semântica (ou seja, o significado) de seus testes já que agrupa testes (ou outros describe) com uma descrição.

O teste em si

É o bloco onde devem ser declarados os testes que realmente serão executados e onde as asserções (que veremos a seguir) serão inseridas.

Através do Jest podem ser acessados através das funções it (que nada mais é que um apelido, ou alias para as funções test).

Dessa forma, podemos criar alguns testes como:

describe('Calculadora', () => {
  it('Soma', () => {

  });
  // ou com test
  test('Soma', () => {

  });
});

Como it e test são iguais, você pode optar pelo que achar melhor.

O que vale a pena ter em mente é que, assim como o describe, utilizar um ou outro pode trazer alguns detalhes mais semânticos pro seu código, já que as funções tem nomenclaturas diferentes.

Asserções (ou assertions)

Asserção significa afirmação. É com as asserções que iremos afirmar o comportamento esperado do nosso código. Assim como os blocos de describe e it, são funções que possuem uma nomenclatura semântica. Podemos pensar na utilização das asserções em duas partes:

  • valor atual (ou de entrada/esperado);
  • valor a ser comparado (ou de saída/resultado).

Com base nesses dois valores, conseguimos sempre organizar nossos testes. Vamos voltar ao exemplo da calculadora e vamos tentar implementar o teste da função de soma (não vamos realizar a função em si).

Podemos criar nosso bloco de descrição (describe) com um teste (it) para iniciar:

import calculadora from './calculadora';

describe('Calculadora', () => {
  it('Soma', () => {
    // faremos o teste aqui
  });
});

Vamos imaginar que teremos um objeto calculadora que possui o método soma, que recebe como parâmetro dois valores que serão somados. Dessa forma, acessamos a função utilizando calculadora.soma.

Com isso, podemos já criar algumas variáveis que serão responsáveis por conter nosso valor esperado e nosso resultado.

Continuando o teste:

import calculadora from './calculadora';

describe('Calculadora', () => {
  it('Soma', () => {
    // atual (a ser testado)
    const atual = calculadora.soma(1, 1);
    // resultado esperado
    const resultado = 2;

    // asserção será feita a seguir
  });
});

Já temos as variáveis com o retorno função (atual) e o resultado esperado (resultado).

Com isso podemos escrever nossa asserção. A função é iniciada com expect (pois iremos consultar e esperar que algum valor seja igual a algo que determinamos) e essa função retorna um objeto com outras funções (chamadas matchers onde verificamos valores correspondentes), como: toEqual (verifica se um valor é igual a outro), toThrow (verifica se um erro foi disparado), toBeGreaterThan (verifica se o valor é maior que outro).

Caso tenha precise, você também pode criar seus matchers customizados.

Nesse exemplo, podemos utilizar o toEqual já que vamos comparar se o valor retornado por nossa função é igual a 2.

import calculadora from './calculadora';

describe('Calculadora', () => {
  it('Soma', () => {
    // atual (a ser testado)
    const atual = calculadora.soma(1, 1);
    // resultado esperado
    const resultado = 2;

    // asserção feita
    expect(atual).toEqual(esperado);
  });
});

Poderíamos ter moldado o teste de qualquer forma, inclusive removendo as variáveis e passando os valores diretamente. Mas acho que dividindo os testes nessas etapas (entrada, saída e execução da função) é uma forma prática de entender como tudo funciona.

Sobre a semântica e a leitura dos testes que comentamos, podemos parar para fazer uma leitura sobre o teste que criamos. Ao ler o código desenvolvido, podemos ler algo como:

Descrição: calculadora
Ela: soma
Espero: que o valor atual (2) seja igual ao esperado (resultado da calculadora.soma(1, 1));

Com isso, conseguimos ver que além de nos trazer mais confiança, escrever testes que se assemelham à forma como seu software é utilizado é uma forma também de documentar seu sistema. Se seus testes estão organizados, você consegue executá-los, ler suas descrições e entender como seu sistema funciona.

Hooks

Outra estrutura que as ferramentas (como o Jest) costumam disponibilizar, são funções executadas em determinada "parte" dos seus testes. São bem úteis caso você precise ajustar/resetar alguma configuração antes, depois ou a cada teste, você consegue utilizá-las para realizar esse trabalho.

Podemos utilizar as funções:

  • beforeAll: para executar algo antes de todos os testes;
  • afterAll: para executar algo após todos os testes finalizarem;
  • beforeEach: para executar algo antes de cada um dos testes;
  • afterEach: para executar algo após cada um dos testes executarem.

Para utilizar essas funções, basta executarmos elas antes dos nossos códigos, algo como:

import calculadora from './calculadora';

describe('Calculadora', () => {
  beforeAll(() => {
    // executa algum trecho de código
    calculadora.limpa();
  });

  it('Soma', () => {
    // atual (a ser testado)
    const atual = calculadora.soma(1, 1);
    // resultado esperado
    const resultado = 2;

    // asserção feita
    expect(atual).toEqual(esperado);
  });
});

A documentação do Jest tem uns exemplos bem bacanas se quiser dar uma lida.

Arquivos

Por fim, apenas um detalhe que vale ser comentado, é comum que alguns arquivos de teste sejam escritos dentro de um diretório como __test__ ou então com o sufixo .test ou .spec (que quer dizer especificação).


E você, já trabalhou com testes? 🧪

Conhecia essas estruturas e já teve experiências com testes? Tem alguma ferramenta favorita que gosta de utilizar e quer comentar?

Espero que tenha curtido e que essas estruturas possam ajudar você a entender e organizar melhor seus testes.


Compartilhe