You’re browsing the documentation for Vue Test Utils for Vue v2.x and earlier.

To read docs for Vue Test Utils for Vue 3, click here.

Guias

Começar agora

O que é o Vue Test Utils?

A Vue Test Utils (VTU) é um conjunto de funções utilitárias com o fim de simplificar os testes de componentes de Vue.js. Ele fornece alguns métodos para montar e interagir com componentes de Vue.js em um modo isolado.

Vamos ver um exemplo:

// Importa o método `mount()` do Vue Test Utils
import { mount } from '@vue/test-utils'

// O componente para testar
const MessageComponent = {
  template: '<p>{{ msg }}</p>',
  props: ['msg']
}

test('displays message', () => {
  // mount() retorna um componente de Vue envolvido com qual podemos interagir
  const wrapper = mount(MessageComponent, {
    propsData: {
      msg: 'Hello world'
    }
  })

  // Afirma o texto renderizado do componente
  expect(wrapper.text()).toContain('Hello world')
})

Os componentes montados são retornados dentro de um Wrapper (envolvedor), o qual expõe métodos para consulta e interação com o componente sob teste.

Simulando a Interação do Usuário

Vamos imaginar um componente contador que incrementa quando o usuário clica no botão:

const Counter = {
  template: `
    <div>
      <button @click="count++">Add up</button>
      <p>Total clicks: {{ count }}</p>
    </div>
  `,
  data() {
    return { count: 0 }
  }
}

Para simular o comportamento, nós precisamos primeiro localizar o botão com o wrapper.find(), o qual retorna um envolvedor para o elemento button. Nós podemos então simular o clique ao chamar .trigger() no envolvedor do botão:

test('increments counter value on click', async () => {
  const wrapper = mount(Counter)
  const button = wrapper.find('button')
  const text = wrapper.find('p')

  expect(text.text()).toContain('Total clicks: 0')

  await button.trigger('click')

  expect(text.text()).toContain('Total clicks: 1')
})

Repare como o teste deve ser async e que o trigger precisa ser esperado. Consulte o guia Testando Comportamento Assíncronos para entender porquê isto é necessário e outras coisas a considerar quando estiver testando cenários assíncronos.

O que se segue

Consulte as nossas dicas comuns para quando estiver escrevendo testes.

Por outro lado, você pode explorar a API completa.

Dicas Comuns

Conhecendo O Que Testar

Para componentes de UI, nós não recomendamos buscar por cobertura completa baseada em linha, porque isto leva focar muito em detalhes de implementação internas dos componentes e poderia resultar em testes frágil.

Ao invés disso, nós recomendamos escrever testes que afirmam a interface publica do seu componente, e tratar seu interior como uma caixa preta. Um único caso de teste afirmaria que alguma entrada (interação de usuário ou mudar de propriedades) fornecida para os resultados do componente na saída esperada (renderizar o resultado ou emitir eventos personalizados).

Por exemplo, imagine um componente Counter o que incrementa por 1 o contador exibido toda vez que um botão é clicado. Seu caso de teste simularia o clique e afirmar que a saída renderizada foi incrementada por 1. O teste não deve cuidar em como o Counter incrementa o valor – ele apenas cuida da entrada e saída.

O beneficio desta abordagem é que contanto que a interface publica do componente continua o mesmo, os seus testes passarão não importa como implementação interna do componente mude ao longo do tempo.

Este tópico é discutido como mais detalhes em grande apresentação feita pelo Matt O'Connell.

Montagem Superficial

Algumas vezes, a montagem de um componente inteiro com todas suas dependências pode se tornar lento ou pesado. Por exemplo, componentes que contém vários componentes filho.

A Vue Test Utils permite você montar um componente sem a renderizar seus componentes filhos (ao forjar eles) com o método shallowMount.

import { shallowMount } from '@vue/test-utils'
import Component from '../Component.vue'

const wrapper = shallowMount(Component)

Tal como o método mount, ele cria um Wrapper que contém o componente de Vue renderizado e montado, mas com componentes filhos forjados.

Repare que usando shallowMount fará o componente sob testes diferente do componente que você executa em sua aplicação - algumas de suas partes não será renderizada! Isto é porque não é maneira sugerida de testar componentes a menos que você enfrente problemas de desempenho ou precisar simplificar teste os planos.

Gatilhos do Ciclo de Vida

Quando estiver usando os métodos mount ou shallowMount, você pode esperar que seu componente para responder para todos eventos ciclos de vida. No entanto, é importante notar que o beforeDestroy e destroyed não serão acionadas a menos que o componente seja manualmente destruído usando o Wrapper.destroy().

Adicionalmente, o componente não será automaticamente destruído no final de cada spec, e está sobre o usuário forjar ou manualmente limpar as tarefas que continuam a executar (setInterval ou setTimeout, por exemplo) antes do final da spec.

Escrevendo testes assíncronos (novo)

Por padrão, as atualizações dos lotes de Vue executam assincronamente (no próximo "tique"). Isto é para prevenir re-renderizações desnecessárias do DOM, e observar computações (consulte a documentação para mais detalhes).

Isto significa que você deve esperar pelas atualização executam depois de você mudar uma propriedade reativa. Você pode fazer isso ao esperar métodos de mutações como trigger:

it('updates text', async () => {
  const wrapper = mount(Component)
  await wrapper.trigger('click')
  expect(wrapper.text()).toContain('updated')
  await wrapper.trigger('click')
  expect(wrapper.text()).toContain('some different text')
})

// Ou se você está sem async/await
it('render text', done => {
  const wrapper = mount(TestComponent)
  wrapper.trigger('click').then(() => {
    expect(wrapper.text()).toContain('updated')
    wrapper.trigger('click').then(() => {
      expect(wrapper.text()).toContain('some different text')
      done()
    })
  })
})

Saiba mais em Testando Comportamento Assíncrono

Afirmando Eventos Emitidos

Cada envolvedor (wrapper) montado automaticamente regista todos eventos emitidos por baixo da instância da Vue. Você pode recuperar os eventos registados usando o método wrapper.emitted():

wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)

/*
`wrapper.emitted()` retorna o seguinte objeto:
{
  foo: [[], [123]]
}
*/

Você pode depois fazer afirmações baseadas nestes dados:

// afirma que o evento foi emitido
expect(wrapper.emitted().foo).toBeTruthy()

// afirma a conta do evento
expect(wrapper.emitted().foo.length).toBe(2)

// afirma a carga do evento
expect(wrapper.emitted().foo[1]).toEqual([123])

Você pode também receber um arranjo (Array) de eventos na ordem de emissão deles ao chamar wrapper.emittedByOrder().

Emitindo Evento a partir do Componente Filho

Você pode emitir um evento personalizado a partir de um componente filho ao acessar a instância.

Componente sob teste

<template>
  <div>
    <child-component @custom="onCustom" />
    <p v-if="emitted">Emitted!</p>
  </div>
</template>

<script>
  import ChildComponent from './ChildComponent'

  export default {
    name: 'ParentComponent',
    components: { ChildComponent },
    data() {
      return {
        emitted: false
      }
    },
    methods: {
      onCustom() {
        this.emitted = true
      }
    }
  }
</script>

Teste

import { mount } from '@vue/test-utils'
import ParentComponent from '@/components/ParentComponent'
import ChildComponent from '@/components/ChildComponent'

describe('ParentComponent', () => {
  it("displays 'Emitted!' when custom event is emitted", () => {
    const wrapper = mount(ParentComponent)
    wrapper.findComponent(ChildComponent).vm.$emit('custom')
    expect(wrapper.html()).toContain('Emitted!')
  })
})

Manipulando o Estado de Componente

Você pode manipular o estado do componente diretamente usando o método setData ou o método setProps no envolvedor (wrapper):

it('manipulates state', async () => {
  await wrapper.setData({ count: 10 })

  await wrapper.setProps({ foo: 'bar' })
})

Imitando Propriedades

Você pode passar as propriedades para o componente usando opção propsData embutida da Vue:

import { mount } from '@vue/test-utils'

mount(Component, {
  propsData: {
    aProp: 'some value'
  }
})

Você pode também atualizar as propriedades de um componente já montado com o método wrapper.setProps({}).

Para uma lista completa das opções, consulte a seção opções de montagem da documentação.

Imitando Transições

Apesar das chamadas de await Vue.nextTick() funcionarem bem para a maioria dos casos de uso, existem algumas situações onde soluções adicionais são necessárias. Estes problemas serão resolvidos antes da biblioteca vue-test-utils sair da beta. Um destes exemplos é o teste unitário de componentes com o envolvedor <transition> fornecido pela Vue.

<template>
  <div>
    <transition>
      <p v-if="show">Foo</p>
    </transition>
  </div>
</template>

<script>
export default {
  data() {
    return {
      show: true
    }
  }
}
</script>

Você pode querer escrever um teste que verifica que se Foo está exibido, então quando o show é definido para false, o Foo não é mais renderizado. Tal teste poderia ser escrito como o seguinte:

test('should render Foo, then hide it', async () => {
  const wrapper = mount(Foo)
  expect(wrapper.text()).toMatch(/Foo/)

  await wrapper.setData({
    show: false
  })

  expect(wrapper.text()).not.toMatch(/Foo/)
})

Na prática, apesar de nós estarmos chamando e chamando o setData para garantir que o DOM é atualizado, este teste falha. Isto é um problema em andamento relacionado a como a Vue implementa o componente <transition>, isto nós gostaríamos de resolver antes da versão 1.0. Por agora, existem algumas soluções:

Usando o auxiliar transitionStub

const transitionStub = () => ({
  render: function(h) {
    return this.$options._renderChildren
  }
})

test('should render Foo, then hide it', async () => {
  const wrapper = mount(Foo, {
    stubs: {
      transition: transitionStub()
    }
  })
  expect(wrapper.text()).toMatch(/Foo/)

  await wrapper.setData({
    show: false
  })

  expect(wrapper.text()).not.toMatch(/Foo/)
})

Isto sobrescreve o comportamento padrão do componente <transition> e renderiza os filhos assim que condição booleana relevante mudar, visto que é o oposto de aplicar classes de CSS, que é como componente <transition> da Vue funciona.

Evitar setData

Uma outra alternativa é simplesmente evitar usar o setData ao escrever dois testes, com a configuração necessária realizada usando as opções mount ou shallowMount:

test('should render Foo', async () => {
  const wrapper = mount(Foo, {
    data() {
      return {
        show: true
      }
    }
  })

  expect(wrapper.text()).toMatch(/Foo/)
})

test('should not render Foo', async () => {
  const wrapper = mount(Foo, {
    data() {
      return {
        show: false
      }
    }
  })

  expect(wrapper.text()).not.toMatch(/Foo/)
})

Aplicando Plugins Globais e Mixins

Alguns componentes podem depender de funcionalidades injetadas por um plugin global ou mixin, por exemplo a vuex e a vue-router.

Se você estiver escrevendo testes para componentes em uma aplicação especifica, você pode configurar os mesmos plugins globais e mixins de uma vez na entrada de seus testes. Mas em alguns casos, por exemplo testando um conjunto de componente genérico que podem ser partilhados entre aplicações diferentes, é melhor testar seus componentes em uma configuração mais isolada, sem poluir o construtor global da Vue. Nós podemos usar o método createLocalVue para alcançar isso:

import { createLocalVue, mount } from '@vue/test-utils'

// cria construtor de `Vue` estendido
const localVue = createLocalVue()

// instala os plugins como normais
localVue.use(MyPlugin)

// passa o `localVue` para as opções do `mount`
mount(Component, {
  localVue
})

Repare que alguns plugins, como a Vue Router, adicionam propriedades de somente leitura ao construtor global da Vue. Isto torna impossível reinstalar o plugin em construtor localVue, ou adicionar imitações para estas propriedades de somente leitura.

Imitando Injeções

Uma outra estratégia para propriedades injetadas é simplesmente imitá-las. Você fazer isso com a opção mocks:

import { mount } from '@vue/test-utils'

const $route = {
  path: '/',
  hash: '',
  params: { id: '123' },
  query: { q: 'hello' }
}

mount(Component, {
  mocks: {
    // adiciona objeto `$route` imitado para a instância da Vue
    // antes da montagem do componente
    $route
  }
})

Forjando componentes

Você pode sobrescrever componentes que são registados globalmente ou localmente usando a opção stubs:

import { mount } from '@vue/test-utils'

mount(Component, {
  // Resolverá o `globally-registered-component` com o
  // forjado vazio
  stubs: ['globally-registered-component']
})

Lidando com o Roteamento

Visto que o roteamento por definição tem haver com toda estrutura da aplicação e envolve vários componentes, ele é melhor testado através de integração ou testes fim-à-fim (end-to-end). Para componentes individuais que dependem de funcionalidades da vue-router, você pode imitá-las usando as técnicas mencionadas acima.

Detetando estilos

O seu teste apenas pode detetar estilos em linha quando estiver executando no jsdom.

Testando eventos de Teclado, Rato e outros do DOM

Acionar eventos

O Wrapper expõe um método trigger assíncrono. Ele pode ser usado para acionar eventos do DOM.

test('triggers a click', async () => {
  const wrapper = mount(MyComponent)

  await wrapper.trigger('click')
})

Você deve estar ciente de que o método find retorna também um Wrapper. Assumindo que o MyComponent contém um botão, o seguinte código clica neste botão.

test('triggers a click', async () => {
  const wrapper = mount(MyComponent)

  await wrapper.find('button').trigger('click')
})

Opções

O método trigger recebe um objeto options opcional. As propriedades dentro do objeto options são adicionadas ao Event.

Repare que o alvo não pode ser adicionado dentro do objeto options.

test('triggers a click', async () => {
  const wrapper = mount(MyComponent)

  await wrapper.trigger('click', { button: 0 })
})

Exemplo de Clique do Rato

Componente sob teste

<template>
  <div>
    <button class="yes" @click="callYes">Yes</button>
    <button class="no" @click="callNo">No</button>
  </div>
</template>

<script>
  export default {
    name: 'YesNoComponent',

    props: {
      callMe: {
        type: Function
      }
    },

    methods: {
      callYes() {
        this.callMe('yes')
      },
      callNo() {
        this.callMe('no')
      }
    }
  }
</script>

Testar

import YesNoComponent from '@/components/YesNoComponent'
import { mount } from '@vue/test-utils'
import sinon from 'sinon'

it('Click on yes button calls our method with argument "yes"', async () => {
  const spy = sinon.spy()
  const wrapper = mount(YesNoComponent, {
    propsData: {
      callMe: spy
    }
  })
  await wrapper.find('button.yes').trigger('click')

  spy.should.have.been.calledWith('yes')
})

Exemplo de Teclado

Componente sob teste

Este componente permite incrementar/decrementar a quantidade usando várias teclas.

<template>
  <input type="text" @keydown.prevent="onKeydown" v-model="quantity" />
</template>

<script>
  const KEY_DOWN = 40
  const KEY_UP = 38
  const ESCAPE = 27

  export default {
    data() {
      return {
        quantity: 0
      }
    },

    methods: {
      increment() {
        this.quantity += 1
      },
      decrement() {
        this.quantity -= 1
      },
      clear() {
        this.quantity = 0
      },
      onKeydown(e) {
        if (e.keyCode === ESCAPE) {
          this.clear()
        }
        if (e.keyCode === KEY_DOWN) {
          this.decrement()
        }
        if (e.keyCode === KEY_UP) {
          this.increment()
        }
        if (e.key === 'a') {
          this.quantity = 13
        }
      }
    },

    watch: {
      quantity: function(newValue) {
        this.$emit('input', newValue)
      }
    }
  }
</script>

Testar

import QuantityComponent from '@/components/QuantityComponent'
import { mount } from '@vue/test-utils'

describe('Key event tests', () => {
  it('Quantity is zero by default', () => {
    const wrapper = mount(QuantityComponent)
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Up arrow key increments quantity by 1', async () => {
    const wrapper = mount(QuantityComponent)
    await wrapper.trigger('keydown.up')
    expect(wrapper.vm.quantity).toBe(1)
  })

  it('Down arrow key decrements quantity by 1', async () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    await wrapper.trigger('keydown.down')
    expect(wrapper.vm.quantity).toBe(4)
  })

  it('Escape sets quantity to 0', async () => {
    const wrapper = mount(QuantityComponent)
    wrapper.vm.quantity = 5
    await wrapper.trigger('keydown.esc')
    expect(wrapper.vm.quantity).toBe(0)
  })

  it('Magic character "a" sets quantity to 13', async () => {
    const wrapper = mount(QuantityComponent)
    await wrapper.trigger('keydown', {
      key: 'a'
    })
    expect(wrapper.vm.quantity).toBe(13)
  })
})

Limitações

O nome da tecla depois do ponto keydown.up é traduzido para um KeyCode (código da tecla). Isto é suportado para os seguintes nomes:

nome da tecla código da tecla
enter 13
esc 27
tab 9
space 32
delete 46
backspace 8
insert 45
up 38
down 40
left 37
right 39
end 35
home 36
pageup 33
pagedown 34

Testando Comportamento Assíncrono

Existem dois tipos de comportamentos assíncronos que você encontrará em seus testes:

  1. Atualizações aplicadas pelo Vue
  2. Comportamento assíncrono fora do Vue

Atualizações aplicadas pela Vue

O Vue agrupa atualizações pendentes da DOM e aplica elas assincronamente para prevenir re-renderizações desnecessárias causadas por várias mutações de dados.

Você pode ler mais sobre atualizações assíncronas na documentação do Vue

Na prática, isto significa que depois da mutação de uma propriedade reativa, para confirmar aquela mudança o seu teste tem que aguardar enquanto o Vue estiver desempenhando atualizações. Uma maneira é usar o await Vue.nextTick(), mas uma maneira mais fácil e clara é apenas esperar (await) o método que com qual você mudou o estado, tipo trigger.

// dentro do conjunto de teste, adicione este caso de teste
it('button click should increment the count text', async () => {
  expect(wrapper.text()).toContain('0')
  const button = wrapper.find('button')
  await button.trigger('click')
  expect(wrapper.text()).toContain('1')
})

Esperar o acionador acima é o mesmo que fazer:

it('button click should increment the count text', async () => {
  expect(wrapper.text()).toContain('0')
  const button = wrapper.find('button')
  button.trigger('click')
  await Vue.nextTick()
  expect(wrapper.text()).toContain('1')
})

Os métodos que podem ser esperados são:

Comportamento assíncrono fora do Vue

Um dos comportamentos assíncrono mais comuns fora de Vue é a chamada de API dentro de ações de Vuex. Os seguintes exemplos mostram como testar um método que faz uma chama de API. Este exemplo usa o Jest para executar o teste e imitar a biblioteca de HTTP axios. Mais sobre as imitações manuais de Jest podem ser encontradas aqui.

A implementação da imitação do axios parece com isto:

export default {
  get: () => Promise.resolve({ data: 'value' })
}

O componente abaixo faz uma chamada de API quando um botão é clicado, depois atribui a resposta ao value.

<template>
  <button @click="fetchResults">{{ value }}</button>
</template>

<script>
  import axios from 'axios'

  export default {
    data() {
      return {
        value: null
      }
    },

    methods: {
      async fetchResults() {
        const response = await axios.get('mock/service')
        this.value = response.data
      }
    }
  }
</script>

Um teste pode ser escrito da seguinte maneira:

import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'
jest.mock('axios', () => ({
  get: Promise.resolve('value')
}))

it('fetches async when a button is clicked', () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  expect(wrapper.text()).toBe('value')
})

Este teste atualmente falha porque a afirmação é chamada antes de resolver a promessa em fetchResults. A maioria das bibliotecas de testes unitários fornecem uma callback (função de resposta) para permitir que o executor saiba quando o teste está completo ou terminado. Ambos Jest e Mocha usam o done. Nós podemos usar o done em combinação com o $nextTick ou setTimeout para garantir que quaisquer promessas estão resolvidas antes da afirmação ser feita.

it('fetches async when a button is clicked', done => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  wrapper.vm.$nextTick(() => {
    expect(wrapper.text()).toBe('value')
    done()
  })
})

A razão pela qual o setTimeout permite o teste passar é porque a fila de micro-tarefa onde as funções de resposta de promessa são processadas executam antes da fila de tarefa, onde as funções de respostas de setTimeout são processadas. Isto significa que no momento que a função de resposta de setTimeout executa, quaisquer funções de resposta de promessa na fila de micro-tarefa terão que ser executadas. O $nextTick por outro lado agenda uma micro-tarefa, mas visto que a fila de micro-tarefa é processada no sentido de que o primeiro a entrar é o primeiro a sair isso também garante que a função de resposta de promessa tem sido executada no momento que a afirmação é feita. Para uma explicação mais detalhada consulte a seguinte ligação.

Uma outra solução é usar uma função async e um pacote como o flush-promises. O flush-promises libera todos manipuladores de promessa pendentes resolvidas. Você pode await a chamada de flushPromises para liberar promessas pendentes e melhorar a legibilidade do seu teste.

O teste atualizado parece com isto:

import { shallowMount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')

it('fetches async when a button is clicked', async () => {
  const wrapper = shallowMount(Foo)
  wrapper.find('button').trigger('click')
  await flushPromises()
  expect(wrapper.text()).toBe('value')
})

Esta mesma técnica pode ser aplicada às ações de Vuex, as quais retornam uma promessa por padrão.

Porquê não apenas await button.trigger() ?

Como explicado acima, existe uma diferença entre o tempo que leva para o Vue atualizar seus componentes, e o tempo que leva para uma promessa, como aquela de axios resolver.

Um ótima regra para seguir é sempre esperar (await) em mutações como o trigger ou o setProps. Se o seu código depende de algo assíncrono, como a chamada de axios, adicione uma espera (await) para a chamada flushPromises também.

Usando com o TypeScript

Um exemplo de projeto para este configuração está disponível no GitHub.

O TypeScript é um superconjunto popular de JavaScript que adiciona tipos e classes sobre o JavaScript habitual. A Vue Test Utils inclui tipos dentro do pacote distribuído, assim ela funciona bem com o TypeScript.

Neste guia, nós iremos caminhar através de como definir uma configuração de estes para um projeto de TypeScript usando Jest e a Vue Test Utils a partir de uma configuração básica de TypeScript da Vue CLI.

Adicionado o TypeScript

Primeiro, você precisa criar um projeto. Se você não tiver a Vue CLI instalada, instale ela globalmente:

$ npm install -g @vue/cli

E crie um projeto ao executar:

$ vue create hello-world

No pronto da CLI, escolha a opção Manually select features, selecione TypeScript, e pressione Enter. Isto criará um projeto com o TypeScript já configurado.

NOTA

Se você quiser mais um guia mais detalhado sobre a configuração de Vue com o TypeScript, consulte o guia de iniciação de Vue com TypeScript.

O próximo passo é adicionar o Jest ao projeto.

Configurando o Jest

O Jest é um executor de teste desenvolvido pelo Facebook, com o propósito de entregar uma solução de testes unitários com baterias incluídas. Você pode aprender mais sobre o Jest na sua documentação oficial.

Instale o Jest e a Vue Test Utils:

$ npm install --save-dev jest @vue/test-utils

Depois defina um roteiro test:unit dentro de package.json.

// package.json
{
  // ..
  "scripts": {
    // ..
    "test:unit": "jest"
  }
  // ..
}

Processando Componentes de Único Ficheiro dentro do Jest

Para ensinar o Jest a como processar ficheiros *.vue, nós precisamos instalar e configurar o pré-processador vue-jest:

npm install --save-dev vue-jest

Depois, criar um bloco de jest dentro de package.json:

{
  // ...
  "jest": {
    "moduleFileExtensions": [
      "js",
      "ts",
      "json",
      // diz ao Jest para manipular ficheiros `*.vue`
      "vue"
    ],
    "transform": {
      // processa ficheiros `*.vue` com o `vue-jest`
      ".*\\.(vue)$": "vue-jest"
    },
    "testURL": "http://localhost/"
  }
}

Configurando o TypeScript para o Jest

No sentido de usar ficheiros de TypeScript dentro de testes, nós precisamos configurar o Jest para compilar o TypeScript. Para isso nós precisamos instalar o ts-jest:

$ npm install --save-dev ts-jest

Depois, nós precisamos dizer ao Jest para processar os ficheiros de teste em TypeScript com o ts-jest ao adicionar um entrada sob jest.transform dentro de package.json:

{
  // ...
  "jest": {
    // ...
    "transform": {
      // ...
      // processar os ficheiros `*.ts` com o `ts-jest`
      "^.+\\.tsx?$": "ts-jest"
    }
    // ...
  }
}

Colocação de Ficheiros de Teste

Por padrão, o Jest selecionará recursivamente todos ficheiros que têm uma extensão .spec.js ou .test.js dentro do projeto inteiro.

Para executar o teste de ficheiros com uma extensão .ts, nós precisamos mudar a testRegex na secção de configuração dentro do ficheiro package.json.

Adicione o seguinte ao campo jest dentro de package.json:

{
  // ...
  "jest": {
    // ...
    "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
  }
}

O Jest recomenda a criação de um diretório __tests__ exatamente próximo ao código a ser testado, mas esteja à vontade para estruturar os seus testes como você desejar. Apenas saiba que o Jest criaria um diretório __snapshots__ próximo aos ficheiros de teste que executam testes instantâneos.

Escrevendo um Teste Unitário

Agora que nós temos o projeto configurado, é hora de escrever um teste unitário.

Crie um ficheiro src/components/__tests__/HelloWorld.spec.ts, e adicione o seguinte código:

// src/components/__tests__/HelloWorld.spec.ts
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'

describe('HelloWorld.vue', () => {
  test('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

É tudo o que nós precisamos fazer para ter o TypeScript e a Vue Test Utils trabalhando juntos!

Recursos

Usando com o Vue Router

Instalando o Vue Router dentro de testes

Você nunca deve instalar o Vue Router sobre o construtor base de Vue dentro de testes. A instalação de Vue Router adiciona $route e $router como propriedades de apenas leitura sobre o protótipo de Vue.

Para evitar isso, nós podemos criar um localVue, e instalar o Vue Router sobre ele.

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

shallowMount(Component, {
  localVue,
  router
})

Nota: A instalação de Vue Router sobre um localVue também adiciona o $route e $router como propriedades de apenas leitura ao localVue. Isto significa que você não pode usar a opção mocks para sobrescrever o $route e o $router quando estiver montando um componente usando um localVue com o Vue Router instalado.

Quando você instalar o Vue Router, os componentes router-link e router-view são registados. Isto significa que nós podemos usar eles em qualquer lugar dentro da aplicação sem a necessidade de importar eles.

Quando nós executamos os testes, nós precisamos tornar estes componentes de Vue Router disponíveis para o componente que estamos montando. Há dois métodos de fazer isso.

Usando os stubs

import { shallowMount } from '@vue/test-utils'

shallowMount(Component, {
  stubs: ['router-link', 'router-view']
})

Instalando o Vue Router com o localVue

import { mount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)

mount(Component, {
  localVue,
  router
})

A instância do roteador está disponível para todos componentes filhos, isto é útil para testes de nível de integração.

Imitando o #route e o $router

Algumas vezes você deseja testar aquele componente que faz alguma coisa com parâmetros dos objetos $route e $router. Para fazer isso, você pode passar imitações personalizadas para a instância de Vue.

import { shallowMount } from '@vue/test-utils'

const $route = {
  path: '/some/path'
}

const wrapper = shallowMount(Component, {
  mocks: {
    $route
  }
})

wrapper.vm.$route.path // /some/path

Nota: os valores imitados de $route e $router não estão disponíveis aos componentes filhos, ou forje estes componentes ou use o método localVue.

Conclusão

A instalação de Vue Router adiciona o $route e o $router como propriedades de apenas leitura sobre o protótipo de Vue.

Isso significa que quaisquer testes futuros que tentar imitar o $route ou o $router falhará.

Para evitar isso, nunca instale o Vue Router globalmente quando você estiver executando os testes; use um localVue como detalhado acima.

Usando com a Vuex

Neste guia, iremos ver como testar a Vuex em componentes com a Vue Test Utils, e como testar uma memória da Vuex.

Testando a Vuex em componentes

Imitando Ações

Vamos ver um pouco de código.

Isto é o componente que nós queremos testar. Ele chama as ações da Vuex.

<template>
  <div class="text-align-center">
    <input type="text" @input="actionInputIfTrue" />
    <button @click="actionClick()">Click</button>
  </div>
</template>

<script>
  import { mapActions } from 'vuex'

  export default {
    methods: {
      ...mapActions(['actionClick']),
      actionInputIfTrue: function actionInputIfTrue(event) {
        const inputValue = event.target.value
        if (inputValue === 'input') {
          this.$store.dispatch('actionInput', { inputValue })
        }
      }
    }
  }
</script>

Para o propósito deste teste, nós não temos interesse no que as ações fazem, ou em como a memória se parece. Nós apenas precisamos saber que estas ações estão sendo disparadas quando elas deveriam, e que elas são disparadas com o valor esperado.

Para testar isto, nós precisamos passar uma imitação da memória para a Vue quando nós montamos superficialmente (shallowMount) o nosso componente.

Ao invés de passar a memória para o construtor base da Vue, nós podemos passar ela para um - localVue. Um localVue é um construtor isolado da Vue que nós podemos realizar mudanças sem afetar o construtor global da Vue.

Veremos que isto se parece:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Actions from '../../../src/components/Actions'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Actions.vue', () => {
  let actions
  let store

  beforeEach(() => {
    actions = {
      actionClick: jest.fn(),
      actionInput: jest.fn()
    }
    store = new Vuex.Store({
      actions
    })
  })

  it('dispatches "actionInput" when input event value is "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'input'
    input.trigger('input')
    expect(actions.actionInput).toHaveBeenCalled()
  })

  it('does not dispatch "actionInput" when event value is not "input"', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    const input = wrapper.find('input')
    input.element.value = 'not input'
    input.trigger('input')
    expect(actions.actionInput).not.toHaveBeenCalled()
  })

  it('calls store action "actionClick" when button is clicked', () => {
    const wrapper = shallowMount(Actions, { store, localVue })
    wrapper.find('button').trigger('click')
    expect(actions.actionClick).toHaveBeenCalled()
  })
})

O que está acontecendo aqui? Primeiro nós dizemos a Vue para usar a Vuex com o método localVue. Isto é apenas um envolvedor em volta da Vue.use.

Nós depois fazemos uma imitação da memória ao chamar new Vuex.Store com as nossas imitações de valores. Nós apenas passamos ela para as ações, visto que é tudo com o que nós nos preocupamos.

As ações são funções de imitação da jest. Estas funções de imitação nos dão métodos para afirmar se as ações foram chamadas ou não.

Nós podemos então afirmar em nossos testes que a ação forjada foi chamada quando esperada.

Agora a maneira que nós definimos a memória pode parecer um pouco estranha para você.

Estamos usando beforeEach para garantir que nós temos uma memória limpa antes de cada teste. O beforeEach é um gatilho da mocha que é chamada antes de cada teste. No nosso teste, nós estamos re-atribuindo os valores das variáveis da memória. Se nós não fizéssemos isto, as funções de imitação precisariam ser automaticamente re-definidas. Ela também nos deixa mudar o estado em nossos testes, sem isto afetar os testes futuros.

A coisa mais importante a anotar neste teste é que nós criamos uma imitação da memória da Vuex e depois passamos ela para a Vue Test Utils.

Excelente, agora nós podemos imitar as ações, veremos em imitando os getters.

Imitando os Getters

<template>
  <div>
    <p v-if="inputValue">{{inputValue}}</p>
    <p v-if="clicks">{{clicks}}</p>
  </div>
</template>

<script>
  import { mapGetters } from 'vuex'

  export default {
    computed: mapGetters(['clicks', 'inputValue'])
  }
</script>

Este é um componente razoavelmente simples. Ele renderiza o resultado dos getters clicks e inputValue. Novamente, nós não nos preocupamos com o que os getters retornam – apenas que seus resultados estão sendo renderizado corretamente.

Vejamos o teste:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import Getters from '../../../src/components/Getters'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('Getters.vue', () => {
  let getters
  let store

  beforeEach(() => {
    getters = {
      clicks: () => 2,
      inputValue: () => 'input'
    }

    store = new Vuex.Store({
      getters
    })
  })

  it('Renders "store.getters.inputValue" in first p tag', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(getters.inputValue())
  })

  it('Renders "store.getters.clicks" in second p tag', () => {
    const wrapper = shallowMount(Getters, { store, localVue })
    const p = wrapper.findAll('p').at(1)
    expect(p.text()).toBe(getters.clicks().toString())
  })
})

O teste é similar aos nossas ações de teste. Nós criamos uma imitação de memória antes de cada teste, passamos ele como uma opção quando nós chamamos shallowMount, e afirmar que o valor renderizado pela nossa imitação de getters está sendo renderizada.

Isto é genial, mas se nós quiséssemos verificar se nossos getters estão retornando a parte correta do nosso estado?

Imitando com Módulos

Os módulos são úteis para separação da nossa memória em pedaços gerenciáveis. Eles também exportam os getters. Nós podemos usar estes nos nossos testes.

Vejamos o nosso componente:

<template>
  <div>
    <button @click="moduleActionClick()">Click</button>
    <p>{{moduleClicks}}</p>
  </div>
</template>

<script>
  import { mapActions, mapGetters } from 'vuex'

  export default {
    methods: {
      ...mapActions(['moduleActionClick'])
    },

    computed: mapGetters(['moduleClicks'])
  }
</script>

Componente simples que inclui uma ação e o getter.

E o teste:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import MyComponent from '../../../src/components/MyComponent'
import myModule from '../../../src/store/myModule'

const localVue = createLocalVue()

localVue.use(Vuex)

describe('MyComponent.vue', () => {
  let actions
  let state
  let store

  beforeEach(() => {
    state = {
      clicks: 2
    }

    actions = {
      moduleActionClick: jest.fn()
    }

    store = new Vuex.Store({
      modules: {
        myModule: {
          state,
          actions,
          getters: myModule.getters,
          namespaced: true
        }
      }
    })
  })

  it('calls store action "moduleActionClick" when button is clicked', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const button = wrapper.find('button')
    button.trigger('click')
    expect(actions.moduleActionClick).toHaveBeenCalled()
  })

  it('renders "state.clicks" in first p tag', () => {
    const wrapper = shallowMount(MyComponent, { store, localVue })
    const p = wrapper.find('p')
    expect(p.text()).toBe(state.clicks.toString())
  })
})

Testando uma Memória da Vuex

Existem duas abordagens para testes de uma memória da Vuex. A primeira abordagem é fazer testes unitários de getters, mutações, e ações separadamente. A segunda abordagem é criar uma memória e testar ela. Veremos ambas abordagens.

Para ver como testar uma memória da Vuex, iremos criar um simples memória de contador (counter). A memória terá uma mutação increment e um getter evenOrOdd.

// mutations.js
export default {
  increment(state) {
    state.count++
  }
}
// getters.js
export default {
  evenOrOdd: state => (state.count % 2 === 0 ? 'even' : 'odd')
}

Testando getters, mutações, e ações separadamente

Os getters, mutações, e ações são todas funções de JavaScript, então nós podemos testar elas sem usar Vue Test Utils e Vuex.

O benefício de testar os getters, mutações, e ações separadamente é que seus testes unitários são detalhados. Quando eles falham, você sabe exatamente o que está errado com o seu código. A desvantagem é que você precisará imitar as funções de Vuex, como commit e o dispatch. Isto pode levar para uma situação em que seus testes unitários passam, mas seu código de produção falha porque suas imitações estão incorretas.

Criaremos dois ficheiros de teste, o mutations.spec.js e o getters.spec.js:

Primeiro, vamos testar as mutações de increment:

// mutations.spec.js

import mutations from './mutations'

test('"increment" increments "state.count" by 1', () => {
  const state = {
    count: 0
  }
  mutations.increment(state)
  expect(state.count).toBe(1)
})

Agora vamos testar o getter evenOrOdd. Nós podemos testar ele ao criar uma imitação state, chamando o getter com o state e verificar se ele retorna o valor correto.

// getters.spec.js

import getters from './getters'

test('"evenOrOdd" returns even if "state.count" is even', () => {
  const state = {
    count: 2
  }
  expect(getters.evenOrOdd(state)).toBe('even')
})

test('"evenOrOdd" returns odd if "state.count" is odd', () => {
  const state = {
    count: 1
  }
  expect(getters.evenOrOdd(state)).toBe('odd')
})

Testando uma execução da memória

Um outra abordagem para testar uma memória da Vuex é criar uma execução da memória usando a configuração da memória.

O benefício da criação de uma instância de execução da memória é que nós não temos que imitar nenhuma função da Vuex.

A desvantagem é que quando um teste quebrar, pode ser difícil encontrar onde o problema está.

Vamos escrever um teste. Quando nós criamos uma memória, usaremos o localVue para evitar a poluição da base do construtor da Vue. O teste cria uma memória usando a exportação de store-config.js:

// store-config.js

import mutations from './mutations'
import getters from './getters'

export default {
  state: {
    count: 0
  },
  mutations,
  getters
}
// store-config.spec.js

import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
import storeConfig from './store-config'
import { cloneDeep } from 'lodash'

test('increments "count" value when "increment" is committed', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.state.count).toBe(0)
  store.commit('increment')
  expect(store.state.count).toBe(1)
})

test('updates "evenOrOdd" getter when "increment" is committed', () => {
  const localVue = createLocalVue()
  localVue.use(Vuex)
  const store = new Vuex.Store(cloneDeep(storeConfig))
  expect(store.getters.evenOrOdd).toBe('even')
  store.commit('increment')
  expect(store.getters.evenOrOdd).toBe('odd')
})

Repare que nós usamos o cloneDeep para clonar a configuração da memória antes da criação uma memória com ela. Isto porque a Vuex realiza mutações nas opções do objeto usado para criar a memória. Para ter a certeza que nós temos uma memória limpa em cada teste, nós precisamos clonar o objeto storeConfig.

No entanto, o cloneDeep não é "profunda (deep)" o suficiente para também clonar os módulos da memória. Se a sua storeConfig incluírem os módulos, você precisará passar um objeto para new Vuex.Store(), deste jeito:

import myModule from './myModule'
// ...
const store = new Vuex.Store({ modules: { myModule: cloneDeep(myModule) } })

Recursos