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