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