Testes de UI com Testing Library
Tags: javascript, testing, react
Published: 2020-08-03 11:50:41

*Aviso: isso não é um tutorial*

Acho que qualquer dev com alguns anos de experiência em desenvolvimento web, em algum momento, teve que fazer testes de interface.

Algumas ferramentas bem comuns para esses tipos de testes são Selenium (e outros parecidos como Cypress por exemplo) que são focados no que chamamos de end-to-end tests, que possuem uma API de automatização do browser para simular o uso da página. Eu particularmente não gosto desses testes pois são geralmente muito frágeis. Como são testes de mega-integração do sistema, Além deles serem muito lentos, qualquer módulo do sistema não respondendo perfeitamente pode causar uma falha do teste. Então o desenvolvedor acaba não rodando esses testes na própria máquina, rodando-o somente no servidor de CI. Quando algum teste falha no CI (e certamente vai falhar com bastante frequência), é mais dificil debugar remotamente. Então o dev roda na sua máquina, e surpresa: *works on my machine*! Essa inconsistência e fragilidade acaba inevitavelmente no time decidindo não manter mais esses testes. Já vi isso acontecendo trocentas mil vezes.

Hoje em dia, as libs de frontend modernas, seja React, Angular, Vue, etc., possuem um framework para testar componentes. Não importa qual lib voce escolha, testar seus componentes vai ajudar a garantir que sua UI está funcionando adequadamente. Arriscaria dizer que se voce tem bons testes de unidade no frontend e bons testes de unidade e integração no backend, vai ser suficiente para garantir o mínimo de qualidade e confiabilidade para deploys frequentes e tranquilos. Aqueles testes end-to-end com Selenium ou outra ferramenta qualquer se tornarão desnecessários e não vão ajudar (muito) a identificar bugs mais complexos. Mas enfim, isso é assunto pra outro post. Voltemos aos testes de componentes.

A maioria dessas libs de teste funciona da seguinte forma: o componente o HTML e alguma lógica de interação, e no teste utiliza-se funções para fazer asserções sobre esse markup e lógica, geralmente utilizando selectors. Por exemplo (com React):

```
const Produto = ({ produto }) => (
 <div class="produto">
  <span class="produto-nome">{produto.nome}</span>
  <span class="produto-descricao">{produto.descricao}</span>
  <span class="produto-preco">R${produto.preco}</span>
 </div>
)
```

Estou usando React aqui como exemplo, mas com Vue ou Angular não é muito diferente.

```
describe('Componente Produto', () => {
 it('exibe preço do produto', () => {
  const produto = { nome: 'TV', descricao: 'TV 4k', preco: 2000 };
  const component = render(<Produto produto={produto} />)
  
  const preco = ReactTestUtils.findRenderedDOMComponentWithClass(componente, 'produto-preco');
  
  expect(preco.textContent).toBe('R$ 2000');
 });
})
```

É um exemplo simples, e que funciona razoavelmente bem. Porém, tem um problema nesse teste e que é comum à praticamente todos os frameworks de teste de interface, que é utilizar selectors, no exemplo, class selectors. Esses frameworks forçam o teste a fazer queries no markup via selectors, seja por class, id ou tag html.

Isso causa um certo acoplamento do teste com a implementação. Se voce muda o HTML da página, seu teste quebra, mesmo que o conteúdo em si tenha se mantido. No exemplo acima do produto, se por acaso eu tiver que mudar o nome da classe para outra coisa por alguma necessidade de CSS, isso vai quebrar todos os testes daquele componente sem nenhuma necessidade, pois a funcionalidade não mudou, o que mudou foram detalhes visuais.

### Testing Library

O [Testing Library](https://testing-library.com/docs/intro) tem uma filosofia e abordagem bem diferente nos testes. Sua API não possui uma função para obter um elemento via class, id nem tag. Sua API é voltada ao conteúdo do componente. Reescrevendo o teste acima, ele ficaria da seguinte forma:

```
describe('Componente Produto', () => {
 it('exibe preço do produto', () => {
  const produto = { nome: 'TV', descricao: 'TV 4k', preco: 2000 };
  const componente = render(<Produto produto={produto} />)
  expect(componente.getByText('R$ 2000')).toBeInTheDocument();
 });
})
```

Repare que em momento algum eu fiz uma consulta de selector. O teste simplesmente verifica se o componente está renderizando o preço corretamente. Se o HTML mudar, mas o preço continuar lá, o teste não quebra, como deveria ser.

O Testing-Library tem diferentes funções de consultas, definidas da seguinte forma: `getBy*`, `getAllBy*`, `queryBy*`, `queryAllBy*`, `findBy*` e `findAllBy*`. E existem funções com "sufixos" variados para diferentes tipos de consulta.

No caso de formulários, as funções de consulta disponíveis são por: texto do label `*ByLabelText`, texto do placeholder do input `*ByPlaceholderText`, e pelo value do input `*ByDisplayValue`.

No caso de texto livre, existe o `*ByText`.

Para imagens, existe o`*ByAltText`. E existem outras queries mais genéricas como o `*ByTitle`, para elementos que possuem o atributo `title`, `*ByRole` para consultar elementos por `aria-role`. Essas funções inclusive podem ajudar os testes a guiarem à um HTML mais acessível.

Existe também o `*ByTestId` que te possibilita fazer consultas à elementos com um atributo `data-testid`, porém é preferível usar os outros métodos de consulta e deixar o uso dessa função para casos onde fica muito difícil usar as outras funções dificulta muito o teste.

A [documentação](https://testing-library.com/docs/dom-testing-library/api-queries) das queries explica com mais detalhes como cada uma funciona.

É possível ver que o framework vai te forçar a escrever seus testes de maneira bem diferente, sempre direcionados ao conteúdo da página, e não à detalhes de implementação do HTML. Também acho que deu para notar que essa abordagem possibilita ter testes menos acoplados à implementação e assim bem mais resistentes à mudanças. Além disso, a lib possui implementação para quase todos os frameworks frontend mais conhecidos: [React, Angular, Vue entre outros](https://testing-library.com/docs/dom-testing-library/install). Isso ajuda em casos de projetos usando libs diferentes à terem uma suite de testes mais uniforme e não obrigar as pessoas a aprenderem uma API de testes para cada framework.