Recentemente aqui no trabalho precisávamos criar um plugin para a nossa aplicação Vue.js. Como eu gosto de pensar em testes sempre, logo levantei a bola para que tal plugin tivesse testes. Mas como criar testes para os nossos plugins? Neste texto eu gostaria de compartilhar algumas abordagens e, passo a passo, veremos como testar nossos plugins Vue.
Para essa tarefa nós vamos usar o Vue Test Utils. Eu já escrevi sobre ele aqui no blog e creio que ele seja a melhor lib para nos auxiliar nessa tarefa, pois iremos interagir com nossos componentes num baixo nível, testando se o plugin que criamos faz o que esperamos que ele faça.
Nota: tanto o código quanto o plugin estão usando a versão 2 do nosso querido framework, o conceito se mantém para o Vue 3, mas a implementação não.
O plugin
A seguir, mostro o código do plugin que iremos testar:
import pkg from 'path/to/package.json'
export default {
install (Vue) {
Vue.prototype.$_appVersion = pkg.version
Vue.prototype.$_pick = (keys = [], original = {}) => {
return keys.reduce((acc, currentKey) => {
acc[currentKey] = original[currentKey]
return acc
}, {})
}
Vue.prototype.$_omit = (keys = [], original = {}) => {
return Object.keys(original).reduce((acc, currentKey) => {
if (!keys.includes(currentKey)) {
acc[currentKey] = original[currentKey]
}
return acc
}, {})
}
Vue.directive('click', {
bind (el, binding) {
el.addEventListener('click', (event) => {
binding.value(event)
})
}
})
Vue.filter('toUpper', (value = '') => value.toUpperCase())
}
}
O conceito é simples: o plugin será usado para adicionar alguns métodos e propriedades que serão compartilhados com todos os componentes Vue. Iremos testar também a diretiva personalizada click
que nós criamos e também o filtro toUpper
.
Antes de começarmos a descriminar os testes, precisamos criar uma instância local do Vue, instalar nosso plugin nela e usarmos essa instância para os nossos testes. Para não precisar repetir esse processo, crio uma função factory cujo objetivo é encapsular essas etapas retornar uma instância de mount
.
import { mount, createLocalVue } from '@vue/test-utils'
import ApplicationPlugin from 'path/to/plugin'
const localVue = createLocalVue()
localVue.use(ApplicationPlugin)
const factory = () => {
const component = {
name: 'TestComponent',
template: '<div></div>'
}
return mount(component, {
localVue
})
}
Testando uma propriedade
A seguir, testamos se o nosso plugin adiciona corretamente a versão do package.json
na instância do componente:
it('should the version number be available from $_appVersion property', () => {
const wrapper = factory()
expect(wrapper.vm.$_appVersion).toBe(pkg.version)
})
Primeiramente, criamos uma variável com o resultado de mount
, e depois checamos se a instância do componente possui a propriedade $_appVersion
. Fazemos isso através de wrapper.vm
.
Testando uma função
Para testamos uma função, o processo é o mesmo, a diferença está no fato de que precisamos verificar se a função executa o que esperamos dela. A seguir, os casos de teste para as funções $_pick
e $_omit
.
it('should the $_pick method should be available', () => {
const wrapper = factory()
const testObject = {
name: 'John',
lastName: 'Doe',
age: 30
}
expect(wrapper.vm.$_pick).toBeInstanceOf(Function)
expect(wrapper.vm.$_pick(['name', 'lastName'], testObject)).toEqual({
name: testObject.name,
lastName: testObject.lastName
})
})
it('should the $_omit method should be available', () => {
const wrapper = factory()
const testObject = {
name: 'John',
lastName: 'Doe',
age: 30
}
expect(wrapper.vm.$_omit).toBeInstanceOf(Function)
expect(wrapper.vm.$_omit(['name', 'lastName'], testObject)).toEqual({
age: testObject.age
})
})
Testando uma diretiva
O teste de uma diretiva também é parecido, porém, não há uma forma exata. Para a diretiva que criamos acima, posso testar criando um componente que faz uso dela e mockando o método que eu espero que seja chamado dado a interação do usuário que a diretiva em questão captura.
it('should install the v-click directive', async () => {
const handleClickMock = jest.fn()
const component = {
name: 'TestComponent',
template: '<button v-click="handleClick"> Test button </button>',
methods: {
handleClick: handleClickMock
}
}
const wrapper = mount(component, {
localVue
})
await wrapper.find('button').trigger('click')
expect(handleClickMock).toHaveBeenCalled()
})
Testando um filtro
Por fim, temos o teste para o filter. Aqui iremos seguir a mesma lógica do teste para a diretiva: vamos criar um componente simulando um uso real do filtro e esperando que ela seja processada corretamente.
it('should install the toUpper directive', async () => {
const component = {
name: 'TestComponent',
template: '<p> {{ message | toUpper }} </p>',
data () {
return {
message: 'Hello world'
}
}
}
const wrapper = mount(component, {
localVue
})
expect(wrapper.find('p').text()).toBe('HELLO WORLD')
})
Mudanças para Vue.js 3
No início do post é mencionado que o código acima não se aplica ao Vue 3. O código do nosso plugin precisa ser alterado devido a algumas breaking changes que temos nessa versão:
A função install de um plugin não recebe mais, como primeiro parâmetro, a instância do Vue, mas sim, a instância de app. Mais informações, você pode conferir na documentação oficial;
filters foram removidos. Para termos uma funcionalidade parecida, podemos usar computeds;
Não é mais possível modificar o prototype do Vue, com
Vue.prototype
, para adicionar funções ou propriedades. Para termos a mesma funcionalidade, podemos criar propriedades globalmente, comglobal.config.globalProperties
.
Dito isso, o código do nosso plugin ficaria assim:
import pkg from 'path/to/package.json'
export default {
install (app) {
app.config.globalProperties.$_appVersion = pkg.version
app.config.globalProperties.$_pick = (keys = [], original = {}) => {
return keys.reduce((acc, currentKey) => {
acc[currentKey] = original[currentKey]
return acc
}, {})
}
app.config.globalProperties.$_omit = (keys = [], original = {}) => {
return Object.keys(original).reduce((acc, currentKey) => {
if (!keys.includes(currentKey)) {
acc[currentKey] = original[currentKey]
}
return acc
}, {})
}
app.directive('click', {
beforeMount (el, binding) {
el.addEventListener('click', (event) => {
binding.value(event)
})
}
})
}
}
E os nossos testes, como ficam?
Bem, com relação aos testes, algumas coisas precisam mudar.
Primeiro, com a nova versão do Vue Test Utils, a api de createLocalVue
está depreciada. Agora, para "instalarmos" nosso plugin no componente que estamos usando, precisamos passar o plugin como uma configuração global. A nossa função factory
poderia ficar assim:
import { mount } from '@vue/test-utils'
import ApplicationPlugin from '@/plugin'
const factory = () => {
const component = {
name: 'TestComponent',
template: '<div></div>'
}
return mount(component, {
global: {
plugins: [ApplicationPlugin]
}
})
}
Uma outra mudança é que como não mais temos filters, o código de teste para filters não se torna mais necessário. Porém, é possível refatorar os locais que usam o filter toUpper
, e criar um método global, por exemplo, $_toUpper
, e usar esse método em computeds, como mencionado acima.
Bem pessoal, ficamos por aqui. Espero que este texto tenha sido útil para você. Se curtiu, não esqueça de compartilhá-lo nas suas redes sociais. Até a próxima!
Imagem de fundo criada por Markus Winkler, download pelo Unsplash.