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

7 2

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

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay