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

taikio

Welker Arantes Ferreira

Posted on October 24, 2021

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

💖 💪 🙅 🚩
taikio
Welker Arantes Ferreira

Posted on October 24, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related