Desmistificando testes de unidade no Vue
Nesse artigo vamos aprender a identificar o que precisamos testar nas aplicações VueJS.
Num cenário onde existe integração contínua e deploy contínuo, testes são muito necessários. Afinal, precisamos garantir que o código que estamos entregando funcione como o esperado e sem efeitos colaterais. Não existe um consenso sobre teste de software, alguns querem falar sobre cobrir todas as linhas de código (em inglês, coverage)... ou seja, testar 100% do que foi escrito. Enquanto outros não estão preocupados com cobertura, mas sim em garantir o comportamento. De um jeito ou de outro, o ponto em comum é sempre testar a menor parte da aplicação. E é aí que entram os testes de unidade.
Quase ninguém voaria num avião que não fosse testado, certo? E muito embora um erro cometido em nossas aplicações não seja tão grave quanto uma falha numa aeronave, mas podemos causar grandes prejuízos e isso não é bom. Teste de software é um conteúdo muito vasto, então vamos discutir neste artigo uma visão mais pragmática sobre testes de unidade (teste de unidade - unit test - é um método onde testamos as menores porções de nossa aplicação).
Uma dúvida frequente para as pessoas que começam a escrever testes para seus componentes é: "Ok, o que eu deveria testar?". No dia a dia, trabalhamos com pessoas que testam demais ou de menos e nenhum extremo costuma ser saudável. Ao testar demais (over testing) você acaba testando além dos seus componentes, como APIs do próprio Vue. E testar de menos (under testing) não vai te dar a confiança necessária para rodar uma pipeline de publicação de forma automática. Vamos, então, seguir as boas práticas e testar com qualidade e somente o que é necessário.
Para testes de componentes de UI (user interface), o mais indicado é a escrita de teste baseado nos contratos dos componentes - a API pública. E o próprio componente em si é tratado como uma caixa preta. A ideia é testar que, dado uma entrada - input - (uma prop alterada ou uma interação do usuário), temos a saída - output - esperada. Simples assim. Vamos imaginar um componente que controla a quantidade de um item num carrinho de compras:
<template>
<div>
<label for="qtd">Quantidade</label>
<input type="number" v-model="quantidade">
<button id="incrementar" @click="incrementar">+ 1</button>
<button id="decrementar" @click="decrementar">- 1</button>
</div>
</template>
<script>
export default {
data() {
return {
quantidade: 0,
};
},
methods: {
incrementar() {
this.quantidade += 1;
},
decrementar() {
this.quantidade -= 1;
},
},
};
</script>
Seguindo o indicado pelo Vue Test Utils, vamos escrever testes para cobrir os cenários de interação com o usuário:
- dado o cenário inicial, o usuário clica no botão "+"
Nesse teste, vamos montar o componente, localizar e clicar no botão de incremento, obter o value do input e testar se é o valor esperado:
import { mount } from '@vue/test-utils';
import QuantidadeCarrinho from '@/components/QuantidadeCarrinho.vue';
describe('QuantidadeCarrinho.vue', () => {
test('aumenta a quantidade em um ao clicar no btn +', async () => {
const wrapper = mount(QuantidadeCarrinho);
await wrapper.find('#incrementar').trigger('click');
const input = wrapper.find('input');
const quantidade = input.element.value;
expect(quantidade).toBe('1');
});
});
Perfeito! Exatamente como o esperado. Dado que o valor inicial é zero, pedimos um único incremento e ele passa a ser um. Vamos para o próximo cenário.
- dado o cenário inicial, o usuário altera o valor do input e em seguida clica no botão "+"
test('após definição de um valor no input, aumenta a quantidade em um ao clicar no btn +', async () => {
const wrapper = mount(QuantidadeCarrinho);
const input = wrapper.find('input');
input.setValue('5');
await wrapper.find('#incrementar').trigger('click');
const quantidade = input.element.value;
expect(quantidade).toBe('6');
});
Com o nosso segundo teste escrito, vamos rodar e ver se tudo funciona conforme o esperado:
Opa! Esse não é o comportamento esperado. Analisando nosso componente, é fácil descobrir o motivo. No método incrementar, ele faz:
incrementar() {
this.quantidade += 1;
},
Quando o usuário altera o valor via input, ele passa a ser uma string. E então o JS concatena a string ao invés de fazer a soma. Vamos fazer uma pequena refatoração - Transformar a quantidade num valor inteiro antes de fazer o incremento:
incrementar() {
this.quantidade = parseInt(this.quantidade) + 1;
},
Agora, de volta ao terminal para rodar os testes:
Tudo funciona conforme o esperado. Esse tipo de ajustes e pequenas refatorações são comuns no dia a dia do desenvolvimento de testes. Existe, inclusive, uma cultura de desenvolvimento guiado por testes, chamado TDD (test driven development).
Olhando a documentação oficial , uma das bibliotecas recomendadas para testes é o Vue Test Utils. O próprio Vue Test Utils não recomenda uma abordagem baseada em cobertura de linha, ou seja, garantir que cada linha de código seja testada. Ao seguir com a abordagem de cobertura de linhas de código, focamos nas implementações internas e isso pode gerar testes frágeis. É por isso que preferimos uma abordagem orientada a contratos e interfaces públicas, tratando o componente como uma caixa preta, mas garantindo que o comportamento seja exatamente o desejado, dada às interações com o usuário ou outros componentes e eventos.
Por baixo dos panos, utilizamos o Jest e o próprio Vue Test Utils. Se gostou desse conteúdo e quer saber mais sobre, aqui na Alura temos uma Formação VueJS onde vamos nos aprofundar ainda mais em todo o ecossistema do VueJS.