- 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
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 palavrabranch
temosramo
(ou "galho") como tradução em português o que faz com que essa coluna possa ser confundida com asbranches
do git, então vale ressaltar que são coisas diferentes. Os valores que aparecem nessa coluna não condizem com a cobertura de suabranch
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 blocosif/else
,switch/case
outerná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 linhasnã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.