DEV Community

Cover image for Introdução a Testes Unitários com Jest e Vue Test Utils
Welker Arantes Ferreira
Welker Arantes Ferreira

Posted on • Edited on

Introdução a Testes Unitários com Jest e Vue Test Utils

Introdução

A medida que evoluímos como desenvolvedores eventualmente chegará o momento de lidar com testes automatizados, é algo inevitável e que pode parecer bem intimidador a primeiro momento. Neste tutorial meu objetivo é te mostrar como você pode começar a testar seus componentes desde a configuração do projeto até a implementação de um componente de pesquisa.

Ferramentas Escolhidas

Neste tutorial utilizarei as libs Vue Test Utils e Jest para implementar os testes de unidade. Explicarei brevemente pra que serve cada lib antes de partirmos para o código:

Vue Test Utils

Este é o utilitário de testes oficial do Vuejs. Ele nos permite montar um componente em memória como se estivesse sendo renderizado no browser e a partir daí interagir com ele.

Jest

O Jest é um framework de testes para a linguagem Javascript desenvolvido pelo Facebook. Por ser escrito em Javascript ele funciona bem tanto no back-end com Nodejs quanto no front-end com frameworks como Vuejs, Angular e React. É o Jest que nos permite de fato testar a aplicação.

Criando o Projeto

Utilize o vue-cli para criar o projeto executando o comando abaixo:

vue create vue-tests 
Enter fullscreen mode Exit fullscreen mode

Selecione a primeira opção Default ([Vue 2, babel, eslint]) e pressione Enter.

Obs.: Caso queira, pode selecionar a opção Manually select features e então marcar a opção Unit Testing para adicionar automaticamente as ferramentas de teste ao seu projeto

Configurando Ferramentas de Teste

Se você adicionou o Jest na etapa anterior pode ignorar esta etapa do tutorial.

Seguindo as instruções de instalação da documentação só precisamos executar os comandos abaixo para instalar as ferramentas:

vue add unit-jest 
Enter fullscreen mode Exit fullscreen mode
npm install --save-dev @vue/test-utils 
Enter fullscreen mode Exit fullscreen mode

Agora precisamos criar um script dentro do arquivo package.json para manter o Jest executando e observando os testes unitários, para adicione o script abaixo no seu package.json:

"scripts": { ... "test:watch": "jest --verbose --watch" } 
Enter fullscreen mode Exit fullscreen mode

Obs.: Caso ocorra erro EMFILE: too many open files ao executar os testes será necessário instalar o utilitário Watchman. Para fazer a instalação siga as instruções da documentação oficial.

Implementação inicial do projeto

Antes de começar a implementar os testes vamos criar o layout inicial do componente, assim garantimos que o visual do componente estará de acordo com o que planejamos. Este será um projeto bem simples onde implementaremos um input de pesquisa. Crie um novo diretório dentro de /components chamado search-input e dentro dele um arquivo index.vue com o código abaixo:

<template> <div class="input"> <input type="text" placeholder="Pesquisar..." /> <span class="input__clear">&times;</span> </div> </template> <script> export default { } </script> <style scoped> .input { display: flex; width: 100%; position: relative; } .input input { width: 100%; padding: 8px 22px 8px 10px; font-size: 1rem; border: 1px solid #e3e3e3; background: #fafafa; border-radius: 6px; outline: none; } .input input:focus { border-color: rgb(109, 61, 255); } .input .input__clear { position: absolute; top: 6px; right: 6px; font-size: 18px; color: red; } .input .input__clear:hover { cursor: pointer; } </style> 
Enter fullscreen mode Exit fullscreen mode

Agora abra o arquivo App.js e substitua todo o código pelo código abaixo:

<template> <div id="app"> <div class="component-box"> <search-input /> </div> </div> </template> <script> import SearchInput from './components/search-input/index.vue' export default { name: 'App', components: { SearchInput } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; width: 100%; height: 100vh; display: flex; justify-content: center; align-items: center; } .component-box { width: 30%; } </style> 
Enter fullscreen mode Exit fullscreen mode

Agora rode a aplicação com o comando npm run serve e verifique se obteve um resultado semelhante ao da imagem abaixo:

Layout Inicial do Input de Pesquisa

Implementando os Testes

Agora finalmente podemos começar a implementar os testes a medida que implementamos as funcionalidades do componente. Para começar crie um diretório chamado __tests__ dentro do diretório search-input, destro deste novo diretório crie um arquivo chamado search-input.spec.js.

Para começar a implementação do primeiro teste copie o código abaixo e cole dentro do arquivo de teste:

import { mount } from '@vue/test-utils' import SearchInput from '../index.vue' describe('search-input - Unit', () => { it('should be a vue instance', () => { const wrapper = mount(SearchInput) expect(wrapper.vm).toBeDefined() }) }) 
Enter fullscreen mode Exit fullscreen mode

precisamos importar o método mount de dentro da lib vue-test-utils, pois é ele que nos permite montar o componente e interagir com ele dentro dos testes. O describe define o início de uma nova suíte de testes. O método it define o primeiro teste da suíte. Na primeira linha do it utilizamos o mount para carregar o componente em memória e em seguida utilizamos o expect() do Jest para testar se o componente foi montado corretamente

Obs.: caso queira executar um teste isolado dentro de uma suíte de testes substitua o método it pelo fit

Para ver os testes rodando no seu terminal execute o comando abaixo:

npm run test:watch 
Enter fullscreen mode Exit fullscreen mode

Agora vamos voltar no App.vue e criar uma variável que será utilizada como v-model no componente search-input:

<template> ... <search-input v-model="search" /> </template> <script> ... data() { return { search: '' } } </script> 
Enter fullscreen mode Exit fullscreen mode

Vamos alterar o componente para receber a prop que foi passada como v-model e uma computed property que será atualizada quando a prop for alterada e emitirá o evento de input sempre que algo for digitado no input:

<template> <div class="input"> <input type="text" placeholder="Pesquisar..." v-model="searchQuery" /> <span class="input__clear">&times;</span> </div> </template> <script> export default { props: { value: { type: String, required: true } }, computed: { searchQuery: { get() { return this.value }, set(val) { this.$emit('input', val) } } } } </script> 
Enter fullscreen mode Exit fullscreen mode

Claro que o ideal é que se escreva os testes antes de fazer a implementação, porém quando estamos começando pode ser difícil alcançar este nível de abstração então neste tutorial farei a implementação antes dos testes para facilitar o entendimento.

Como declaramos uma prop que é obrigatória precisamos alterar o nosso primeiro teste para passar uma prop chamada value:

it('should be a vue instance', () => { const wrapper = mount(SearchInput,{ propsData: { value: '' } }) expect(wrapper.vm).toBeDefined() }) 
Enter fullscreen mode Exit fullscreen mode

Agora que temos parte da lógica do componente implementada vamos escrever mais testes. O próximo teste que vamos escrever será para verificar se o valor de searchQuery é alterado quando alteramos o valor da prop value:

it('should update searchQuery when prop value is changed', async () => { const wrapper = mount(SearchInput,{ propsData: { value: '' } }) await wrapper.setProps({ value: 'test' }) await wrapper.vm.$nextTick() expect(wrapper.vm.searchQuery).toEqual('test') }) 
Enter fullscreen mode Exit fullscreen mode

Aqui utilizamos o método setProps para atualizar o valor da prop value e em seguida utilizamos o $nextTick para indicar que é preciso esperar até o próximo ciclo de vida para continuar a execução do teste.

O terceiro teste verificará se o evento de input é emitido quando algo é digitado no elemento input:

it('should emit input event when something is typed', async () => { const wrapper = mount(SearchInput,{ propsData: { value: '' } }) const inputEl = wrapper.find('input[type="text"]') await inputEl.setValue('test') expect(wrapper.emitted().input).toBeTruthy() expect(wrapper.emitted().input[0]).toEqual(['test']) }) 
Enter fullscreen mode Exit fullscreen mode

Neste teste utilizamos o método find para obter o elemento de input e então utilizamos o setValue para atribuir um valor ao elemento. Após preparar o cenário do teste utilizamos o método emitted para verificar se o evento de input foi disparado.

Agora que você já se familiarizou com a ideia de escrever testes unitários que tal começar a implementar os testes antes da funcionalidade?

Vamos criar um teste para verificar se o valor do input é limpo quando clicamos no ícone de X:

it('should clear input value when X icon is clicked', async () => { const wrapper = mount(SearchInput,{ propsData: { value: '' } }) const clearBtn = wrapper.find('.input__clear') await clearBtn.trigger('click') expect(wrapper.emitted().input).toBeTruthy() expect(wrapper.emitted().input[0]).toEqual(['']) }) 
Enter fullscreen mode Exit fullscreen mode

Neste teste utilizamos o método find para acessar o botão de limpar e em seguida utilizamos o trigger para simular o evento de click no botão. Por último verificamos se o evento de input foi disparado com uma string vazia.

Obviamente o teste irá falhar já que não implementamos a funcionalidade. Para fazer o teste passar vamos implementar a funcionalidade de limpar o valor:

<template> ... <span class="input__clear" @click="clearValue()"> &times; </span> </template> <script> ... methods: { clearValue() { this.$emit('input', '') } } </script> 
Enter fullscreen mode Exit fullscreen mode

Conclusão

Se você chegou até aqui deve ter um resultado igual a este:

<template> <div class="input"> <input type="text" placeholder="Pesquisar..." v-model="searchQuery" /> <span class="input__clear" @click="clearValue()">&times;</span> </div> </template> <script> export default { props: { value: { type: String, required: true } }, computed: { searchQuery: { get() { return this.value }, set(val) { this.$emit('input', val) } } }, methods: { clearValue() { this.$emit('input', '') } } } </script> <style scoped> .input { display: flex; width: 100%; position: relative; } .input input { width: 100%; padding: 8px 22px 8px 10px; font-size: 1rem; border: 1px solid #e3e3e3; background: #fafafa; border-radius: 6px; outline: none; } .input input:focus { border-color: rgb(109, 61, 255); } .input .input__clear { position: absolute; top: 6px; right: 6px; font-size: 18px; color: red; } .input .input__clear:hover { cursor: pointer; } </style> 
Enter fullscreen mode Exit fullscreen mode
import { mount } from '@vue/test-utils' import SearchInput from '../index.vue' describe('search-input - Unit', () => { it('should be a vue instance', () => { const wrapper = mount(SearchInput,{ propsData: { value: '' } }) expect(wrapper.vm).toBeDefined() }) it('should update searchQuery when prop value is changed', async () => { const wrapper = mount(SearchInput,{ propsData: { value: '' } }) await wrapper.setProps({ value: 'test' }) await wrapper.vm.$nextTick() expect(wrapper.vm.searchQuery).toEqual('test') }) it('should emit input event when something is typed', async () => { const wrapper = mount(SearchInput,{ propsData: { value: '' } }) const inputEl = wrapper.find('input[type="text"]') await inputEl.setValue('test') expect(wrapper.emitted().input).toBeTruthy() expect(wrapper.emitted().input[0]).toEqual(['test']) }) it('should clear input value when X icon is clicked', async () => { const wrapper = mount(SearchInput,{ propsData: { value: '' } }) const clearBtn = wrapper.find('.input__clear') await clearBtn.trigger('click') expect(wrapper.emitted().input).toBeTruthy() expect(wrapper.emitted().input[0]).toEqual(['']) }) }) 
Enter fullscreen mode Exit fullscreen mode

E assim chegamos ao fim deste tutorial, espero que tenha gostado ;)

Caso queira aprender mais sobre o Vue Test Utils pode dar uma conferida nos guias da própria documentação

Top comments (0)