• #vue
  • #tests

Teste seus componentes com Vue Testing Library

Publicado em Ago, 30 2021 - 12 minutos de leitura


Fala pessoal, hoje gostaria de trazer mais uma ferramenta para testes com Vue.js que é o Testing Library. Essa ferramenta recomendada pelo time do Vue.js possui algumas diferenças em relação ao Vue Test Utils (VTU) e vamos discuti-las neste texto. Só lembrando que já trouxe aqui uma análise a respeito do Vue Test Utils aqui no blog, então também não deixe de conferir!

Conhecendo um pouco do Testing Library (TL)

O Testing Library é uma biblioteca para criação de testes unitários e de integração bastante usada juntamente com o Jest no ecossistema React. Ela possui uma filosofia que particularmente acho interessante. O criador da lib afirma que:

The more your tests resemble the way your software is used, the more confidence they can give you.

Em tradução livre, seria:

Quanto mais seus testes se aproximam da forma como seu software é usado, mais confiança eles podem dar a você.

Segundo o Guiding Principles do Testing Library, o objetivo da lib é expor métodos e utilities (traduzo como funções de utilidade) que nos encorajam a escrever testes que sejam o mais próximo de como o seu software será usado.

Tal objetivo se traduz nos seguintes princípios:

  • Quando o componente for renderizado, você não trabalhará com a instância do componente, mas com o resultado, em HTML, dessa renderização. No caso do Vue.js, creio que eles não aplicam completamente este princípio, expondo a função emitted. Por outro lado, eles não utilizam o shallowMount, apenas o mount do VTU por baixo dos panos.

  • Quando for feita a interação para o teste, ela será feita utilizando métodos que "emulam" o comportamento do usuário. Exemplo: se o componente possui um botão com o texto "Clique em mim" (que exemplo tosco), no teste você deverá utilizar a função getByRole('button', { name: 'Clique em mim' }) para buscar esse botão.

Para ambos os princípios, discutiremos seus prós e contras mais a frente.

Queries

Pessoalmente, a suíte de métodos para a busca de um elemento é uma das coisas que eu mais gosto no TL, chamado por eles de queries.

De maneira geral, uma query é um método que visa buscar um ou mais elementos na página. Há alguns tipos de queries, e para cada query há um trade-off relacionado. Exemplo: se você usar uma query que começa com get, se ela não encontrar o elemento, ela retorna um erro. Se você espera que o elemento não exista, você deve usar uma que começa com find.

Posso usar qualquer query? Poder, pode, porém, o TL define uma ordem de prioridade em que o foco é a semântica do elemento que você quer encontrar. Vejamos rapidamente:

  1. Use queries em que os elementos são acessíveis a todos, tanto para usuários que usam tecnologias assistivas quanto para quem não as usa. Dentro dessa categoria, podemos elencar as seguintes queries:

    1. byRole: use essa query quando você deseja procurar um elemento dentro da árvore de acessibilidade (accessibility tree) do site. Ela é a primeira da lista pois o foco é na acessibilidade: se seu elemento não é encontrado usando essa query, é provável que ele não seja acessível.

    2. byLabelText: essa é voltada para o contexto de formulários. É bastante recomendado que um item de formulário possua uma label que o identifique. Essa query ajuda a encontrar tal elemento.

    3. byPlaceholderText: segundo a própria documentação, um placeholder não é um substituto para labels, porém, pode ser útil em alguns casos.

    4. byText: procure por um elemento que possua um determinado texto.

    5. byDisplayValue: procure por um elemento que possua o valor que você está tentando procurar. Comumente usado para encontrar elementos do tipo input.

  2. As queries acima não te atendem? Ou você precisa ter uma questão específica de semântica? Use uma das queries a seguir:

    1. byAltText: essa query busca um elemento pelo seu atributo alt. Útil no contexto de imagens, por exemplo.

    2. byTitle: busca pelo atributo title

  3. Se ainda as queries acima não forem suficientes, você pode usar uma boa prática de desacoplamento dos testes da implementação, que é usar os atributos testids. Para encontrar um elemento pelo seu atributo data-testid, use o byTestId. Mas perceba que na ordem de prioridade, essa query é a última a ser usada.

Considerações ao uso do TL

Vejamos algumas questões que encontramos ao usarmos essa ferramenta.

Encorajamento no olhar para a acessibilidade

Nossas interfaces não podem ser apenas bonitas, elas precisam ser acessíveis. Sem acessibilidade, podemos estar ignorando uma grande fatia de usuários da nossa aplicação. Creio que nesse quesito o TL marca um gol pois me ajuda a pensar na acessibilidade dos meus componentes, seja a nível micro com testes unitários ou a um nível de teste de integração.

Além disso, eu aprendo um pouco de semântica com ele. Na documentação da query byRole há um link para uma tabela que mostra os elementos HTML e suas roles padrão ou desejáveis. Só de consultar essa tabela já temos uma boa aprendizagem de semântica HTML.

Assim, o TL dá um passo a frente em relação ao Vue Test Utils pois fornece uma forma de testar a semântica e a acessibilidade do seu componente de maneira indireta no seu teste.

Vamos a um exemplo para ilustrar isso. Imagine que temos o seguinte componente de input:

<template>
  <div class="field">
    <label class="label" :for="id">{{ label }}</label>

    <div class="control">
      <input
        class="input"
        :id="id"
        type="text"
        :value="value"
        @input="handleInput"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: 'TextField',
  props: {
    id: {
      type: String,
      default: null
    },
    label: {
      type: String,
      default: null
    },
    value: {
      type: String,
      default: null
    }
  },
  methods: {
    handleInput (event) {
      this.$emit('input', event.target.value)
    }
  }
}
</script>

Como testaríamos esse componente com o TL?

it('should have a label with passed text', async () => {
  const { emitted } = render(TextField, {
    props: {
      label: 'My label',
      value: null,
      id: 'test'
    }
  })

  const data = 'my new value'
  await fireEvent.update(
    screen.getByRole('textbox', { name: /my label/i }),
    data
  )

  expect(emitted().input[0]).toEqual([data])
})

É um teste simples, concordo, mas perceba o que eu estou testando aqui. Estou procurando por um elemento que possui uma role textbox cujo nome na árvore de acessibilidade é "My label". E quando esse elemento é atualizado, o componente emite um evento de input com esse valor. Um accessible name poderia ser um label, que é o caso aqui, mas também um texto de um botão ou o valor do atributo aria-label.

Testes com mais resiliência a mudanças

Eu particularmente enxergo o TL como uma lib que me ajuda a criar testes de integração. Quando eu trouxe para a mesa a discussão sobre o que seria testes de unidade no contexto de aplicações componentizadas, eu trouxe a opinião de que o componente seria a minha unidade.

Se o meu componente for bem enxuto, usar o TL ou o Vue Test Utils não fará muita diferença, a menos que há quesitos de acessibilidade para serem levados em consideração. Porém, em componentes mais complexos, em que principalmente há outros componentes envolvidos, você torna seu teste unitário um pouco mais desacoplado da implementação do componente.

Pegamos como exemplo o caso de teste acima. Eu só preciso testar se o componente possui um input com uma label específica, e ao alterar o valor desse input, eu espero que o componente emita o valor atualizado do input. Se eu alterar no meu componente, por exemplo, o método handleInput e mudar ele, ou até mesmo não usar um método, o meu teste continuará passando porque o comportamento esperado continua sendo atendido.

Melhor suporte a testes de integração

E quando temos um componente complexo? TL nos ajuda aqui pois ele nos encoraja a pensar em como o usuário irá interagir com o nosso componente em questão. Quando eu falo de testes de integração, eu estou falando de testes que tratam de mais de um componente ao mesmo tempo. Vejamos o seguinte exemplo de componente de formulário:

<template>
  <form @submit.prevent>
    <TextField
      data-testid="form-name"
      label="Nome"
      id="name"
      v-model="model.name"
    />

    <TextField
      data-testid="form-last_name"
      label="Sobrenome"
      id="last_name"
      v-model="model.last_name"
    />

    <div class="field" data-testid="form-position">
      <label class="label" for="position">Posição atual</label>

      <div class="control">
        <div class="select">
          <select id="position" v-model="model.position">
            <option value="front">Front End</option>
            <option value="back">Back End</option>
            <option value="devops">DevOps</option>
          </select>
        </div>
      </div>
    </div>

    <div class="buttons">
      <button
        type="button"
        class="button is-is-danger"
        data-testid="btn-cancel"
      >
        Cancelar
      </button>

      <button
        type="submit"
        class="button is-primary"
        data-testid="btn-save"
        @click="handleSubmit"
      >
        Salvar
      </button>
    </div>
  </form>
</template>

<script>
import TextField from './TextField.vue'
export default {
  name: 'Form',
  components: {
    TextField
  },
  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: {
    handleSubmit () {
      if (!this.canSave) {
        return
      }
      this.$emit('save', this.model)
    },
    isNotEmpty (value) {
      return value !== null || (typeof value === 'string' && value.length > 0)
    }
  }
}
</script>

Esse é um formulário simples, que usa o componente que mostramos acima. Como podemos testar ele? Veja o exemplo de código a seguir:

it('should properly fill the model data', async () => {
  const { emitted } = render(Form)

  const name = 'John'
  const lastName = 'Doe'
  const expectedModel = {
    name,
    last_name: lastName,
    position: 'front'
  }

  userEvent.type(
    screen.getByRole('textbox', { name: 'Nome' }),
    name
  )

  userEvent.type(
    screen.getByRole('textbox', { name: 'Sobrenome' }),
    lastName
  )

  userEvent.selectOptions(
    screen.getByRole('combobox', { name: 'Posição atual' }),
    screen.getByText('Front End')
  )

  userEvent.click(
    screen.getByRole('button', { name: 'Salvar' })
  )

  expect(emitted().save[0][0]).toEqual(expectedModel)
})

Perceba que ainda estamos testando um componente em específico. Se lembra que comentei mais cedo que a função render do VTL retorna a função emitted do VTU? Isso é útil porque conseguimos testar o que o nosso componente retorna das interações do usuário.

Voltando ao caso de teste. Perceba que estou testando o meu código de uma forma mais próxima ao do usuário. É praticamente legível e útil para documentações:

  1. Busque por um elemento input com uma label Nome e preencha "John"

  2. Busque por um elemento input com uma label Sobrenome e preencha "Doe"

  3. Busque por um elemento select e selecione a opção "Front End"

  4. Busque por um botão com um texto "Salvar" e execute um clique nele

Interessante e poderoso não?

Testes em baixo nível podem ser difíceis

Em alguns casos específicos pode ser mais complexo testar com o Testing Library. Um dos motivos é que ele utiliza o Vue Test Utils por baixo dos panos, porém, apenas a função mount é usada, ou seja, você não consegue usar uma estratégia de Shallow mounting nos seus testes.

Um outro problema é que você não terá acesso a API do Vue Test Utils, inclusive nem a instância de wrapper nos seus testes. Sendo assim, em alguns cenários em que você precisa pegar um componente filho e interagir com ele e testar esse resultado no componente pai, não será possível com o TL.


Resumindo

Se você está a procura de uma biblioteca que te ajuda a pensar na acessibilidade e semântica, que te ajuda a criar testes mais resilientes ou está num cenário em que alguns componentes que você está testando são um pouco ou bastante complexos, dê uma olhada no TL. Pode ser uma boa alternativa para você.

Aonde se aprofundar mais?


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.


Crédito foto da capa: jonas via Unsplash.