Introdução a Testes Unitários com Jest e Vue Test Utils
Tags: webdev, testing, tutorial, tdd
Published: 2021-10-24 23:32:49

## 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:

```bash
vue create vue-tests
```

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](https://vue-test-utils.vuejs.org/installation/#using-vue-test-utils-with-jest-recommended) só precisamos executar os comandos abaixo para instalar as ferramentas:

```bash
vue add unit-jest
```

```bash
npm install --save-dev @vue/test-utils
```

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:

```json
"scripts": {
...
  "test:watch": "jest --verbose --watch"
}
```

**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](https://facebook.github.io/watchman/docs/install.html).

## 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:

```vue
<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>
```

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

```vue
<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>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j7xipmhm3xcj7r2ncvsi.png)

## 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:

```javascript
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()
  })
})
```

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:

```bash
npm run test:watch
```

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

```vue
<template>
...
  <search-input v-model="search" />
</template>

<script>
...
data() {
    return {
      search: ''
    }
  }
</script>
```

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:

```vue
<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>
```

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:

```javascript
it('should be a vue instance', () => {
  const wrapper = mount(SearchInput,{
    propsData: {
      value: ''
    }
  })

  expect(wrapper.vm).toBeDefined()
})
```

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`:

```javascript
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')
  })
```

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:

```javascript
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'])
  })
```

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**:

```javascript
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([''])
  })
```

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: 

```vue
<template>
  ...
<span class="input__clear" @click="clearValue()">
  &times;
</span>
</template>

<script>
...
methods: {
  clearValue() {
    this.$emit('input', '')
  }
}
</script>
```

## Conclusão

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

```vue
<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>
```

```javascript
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([''])
  })
})
```

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](https://vue-test-utils.vuejs.org/guides/)