Fala! Hoje gostaria de trazer o assunto de testes com Vue.js. Depois de alguns meses trabalhando na criação de uma biblioteca de componentes de maneira intensiva, e agora, trabalhando na migração do produto principal aqui da empresa, gostaria de compartilhar alguns pensamentos e opiniões a respeito de algumas abordagens de testes com Vue.js e o uso de bibliotecas que nos auxiliam na hora de implementá-los.
O objetivo é poder discutir o uso do Vue Test Utils e do Vue Testing Library, vantagens e desvantagens de usar um ou outro e quem sabe, num terceiro momento, trazer o uso do Cypress Component Testing, que promete bastante.
Antes de mais nada, você precisa saber que os códigos deste post estarão neste repositório no Github - vue2-testing-blog. Dá aquela força dando um star no projeto e compartilhando.
Sem mais delongas, vamos ao conteúdo de hoje. Como testar nossos componentes Vue.js com Vue Test Utils!
Primeiramente, vou pular a parte da instalação do Vue Test Utils (daqui para a frente vou chamá-lo de VTU), pois normalmente se você inicia um projeto com o Vue CLI e escolhe entre as opções, que irá ter testes unitários, você vai perceber que este pacote será instalado por padrão. Porém, caso você precise de um guia para a instalação, você pode conferir a seção "Instalação" na documentação oficial.
Conhecendo um pouco do VTU
Quando vamos testar nossos componentes Vue.js, precisamos, ao menos, executar duas tarefas:
Buscar elementos dentro do componente
Interagir com esses elementos
Essas duas tarefas acima tem como objetivo testar se, dado uma interação com um componente, ele executa o que a gente espera dele. Vamos a um exemplo extraído da documentação do VTU
// nosso componente Counter
const Counter = {
template: `
<div>
<button @click="count++">Add up</button>
<p>Total clicks: {{ count }}</p>
</div>
`,
data() {
return { count: 0 }
}
}
// nossa suíte de testes
test('increments counter value on click', async () => {
const wrapper = mount(Counter)
// busca elementos para interação
const button = wrapper.find('button')
const text = wrapper.find('p')
// executa uma asserção para ter um valor inicial
expect(text.text()).toContain('Total clicks: 0')
// interage com um elemento
await button.trigger('click')
// testa se ocorre o resultado esperado
expect(text.text()).toContain('Total clicks: 1')
})
Como você pode ver, executamos as duas tarefas e assim testamos o nosso componente de maneira satisfatória.
Você pode encontrar mais informações sobre essa biblioteca no seu site oficial e encontrar um conjunto de boas práticas no Vue Testing Handbook, criado pelo autor do VTU.
Depois dessa breve introdução, gostaria de compartilhar um pouco das minhas reflexões com relação à essa biblioteca. Já gostaria de adiantar que são minhas opiniões depois de algum tempo usando bastante ela, seja no contexto de uma biblioteca de componentes, seja no contexto de uma aplicação real em construção.
Biblioteca oficial para testes
Uma das coisas que mais incomodam quando vamos usar bibliotecas, plugins ou qualquer outra coisa em conjunto com nosso framework e estamos num processo de migração deste é o suporte que tais pacotes de terceiros irão ter com o framework que estamos utilizando.
Sem dúvida, uma das melhores partes de se usar o VTU é que você vai estar usando uma ferramenta que é mantida pelo core team do Vue.js. Isso significa que qualquer alteração no Vue.js terá impactos e "rapidamente" será propagado para o VTU.
Interação com o componente a baixo nível
Aqui vem uma questão polêmica: o que seria um teste unitário no contexto de aplicações componentizadas que temos hoje em dia? Seria testar uma funcionalidade específica do meu sistema? Seria testar apenas um componente específico, isoladamente?
Se sua resposta a última pergunta foi sim, o VTU é perfeito para você. Ao usarmos a função mount
, ela retorna uma instância de um componente, e assim podemos fazer um genuíno teste unitário caixa branca, pois podemos trabalhar com um componente a baixo nível e testar seu comportamento esperado nos mínimos detalhes.
Mas, o que eu quero dizer com trabalhar com um componente no baixo nível?
Se lembra que eu comentei que a função mount
retorna uma instância do componente? Por essa instância, eu tenho acesso as propriedades do meu componente (data
, computeds
e props
), bem como aos métodos dele. Sendo assim, imagine que meu componente tenha a seguinte estrutura:
// omitindo o template de propósito
export default {
data: () => ({
model: {
name: null,
last_name: null,
position: null
}
}),
computed: {
canSave () {
return (
this.isNotEmpty(this.model.name) &&
this.isNotEmpty(this.model.last_name) &&
this.isNotEmpty(this.model.position)
)
}
},
methods: {
isNotEmpty (value) {
if (typeof value === 'string') {
return value.length > 0
}
return value !== null
}
}
}
Agora eu quero testar que, se eu alterar o model.name
, e o model.last_name
, a computed canSave
ainda precisa ser false
, pois ainda não tenho um valor para model.position
. E eu consigo fazer isso sem muito esforço:
// ignorando importação e montagem do componente
it('should keep the canSave computed as false even if name and last_name are filled', () => {
wrapper.vm.model.name = 'John'
wrapper.vm.model.last_name = 'Doe'
expect(wrapper.vm.canSave).toBe(false)
})
Eu também posso testar separadamente meu método isNotEmpty
:
it('should return the correct values for the vm.isNotEmpty() method', () => {
const wrapper = mount(Form)
expect(wrapper.vm.isNotEmpty('')).toBe(false)
expect(wrapper.vm.isNotEmpty(null)).toBe(false)
expect(wrapper.vm.isNotEmpty('test')).toBe(true)
})
Como consequência dessa interação a baixo nível, eu posso não testar a interação do meu usuário com o componente que eu estou testando. Um exemplo: imagine que você tem um componente de login que possui um método login
(bem criativo 😅). Você pode testar o método login
separadamente. Porém, eu acredito ser melhor testar a interação do usuário com esse método, por exemplo, testando se ao clicar no botão de Login, o método login
é chamado e executa a chamada à API com os parâmetros esperados.
Uma outra consequência dessa interação a baixo nível é que se torna um pouco complexo usar o VTU para realizar buscas por elementos dentro do componente.
Pegue como exemplo, este simples componente de formulário. Imagine que eu precise testar o preenchimento de cada campo do formulário e ao final, precise clicar no botão de Salvar e esperar que o componente emita o que espero dele.
Imaginou? Uma possível solução seria essa:
it('should properly fill the model data', async () => {
const wrapper = mount(Form)
const name = 'John'
const lastName = 'Doe'
const expectedModel = {
name,
last_name: lastName,
position: 'front'
}
await wrapper.find('[data-testid="form-name"] input').setValue(name)
await wrapper
.find('[data-testid="form-last_name"] input')
.setValue(lastName)
await wrapper
.find('[data-testid="form-position"] select')
.findAll('option')
.at(0)
.setSelected()
expect(wrapper.vm.model).toEqual(expectedModel)
await wrapper.find('[data-testid="btn-save"]').trigger('click')
expect(wrapper.emitted('save')[0][0]).toEqual(expectedModel)
})
Aqui eu uso data-testids
- uma boa prática para desacoplar seus testes do código em si - para facilitar essa busca, porém, este problema nos leva a pelo menos dois pontos negativos ao usarmos o VTU.
Não é fluído testar componentes complexos
Uma das minhas maiores dores com o VTU é buscar os elementos para interação, porém, eu preciso reconhecer que isso é uma característica da biblioteca. Ao mesmo tempo que é fácil buscar um componente filho na minha instância e interagir com ele, meu código de teste fica verboso e bastante acoplado à implementação.
Em outras palavras, eu só tenho duas possibilidades de buscar elementos no meu componente: usando um seletor HTML (método find e afins) ou buscando pelo componente em si (usando o método findComponent e afins).
Porém, você talvez concorde que estar acoplado a implementação neste caso não é ruim, na verdade é esperado, pois como estou testando "unitariamente" meu componente, é esperado que eu saiba nos mínimos detalhes como ele funciona, e queira garantir através dos testes seu funcionamento.
É difícil testar acessibilidade
Não é impossível verificar questões de acessibilidade quando usamos o VTU, porém, a biblioteca não encoraja uma abordagem de testes unitários/de integração que ao mesmo tempo, testem questões de acessibilidade.
Um exemplo de teste de acessibilidade é verificar se os meus itens de formulários estão devidamente "rotulados", ou seja, se existem labels que os identificam. Com VTU isso é um pouco complexo. Eu precisaria saber o id
de cada elemento, e verificar se existe uma label com o correspondente atributo for
.
Em um outro momento veremos que existe uma outra biblioteca de testes que provê uma série de queries que trazem fluidez na hora que estamos criando nossos testes, e ao mesmo tempo, encoraja em pensarmos um pouco sobre acessibilidade na criação de nossos componentes.
Resumindo
Se você está a procura de uma biblioteca de auxílio nos testes unitários dos seus componente Vue.js, sem dúvida, o VTU é uma excelente alternativa, pois ele te dará um controle refinado para a criação dos seus testes.
Todavia, é necessário deixar claro que caso o seu componente seja um pouco complexo e tenha interações com outros componentes, será um pouco trabalhoso criar seus testes com o VTU.
Aonde se aprofundar mais?
A documentação do VTU é sucinta e direta ao ponto, comece por ela.
Se está a procura de uma abordagem mais "mão na massa", não deixe de conferir o Handbook do VTU, que traz alguns cases e algumas boas práticas na criação de testes com o VTU.
Nas minhas pesquisas eu encontrei este Cheatsheet que achei bem interessante. Pode servir como uma referência.
Espero que este post tenha sido útil para você e que ele possa ter trazido algumas reflexões a respeito dessa excelente biblioteca de testes com Vue.js.
Até a próxima.