DEV Community

Adriana Ferreira Lima Shikasho
Adriana Ferreira Lima Shikasho

Posted on • Edited on

[pt-BR] Quer testar sua interface? Entenda o que não fazer - #1

Os testes compõe uma parte fundamental no processo de desenvolvimento de software. Apesar disso, muita gente acha os testes desagradáveis, principalmente os testes de interface.

Existem várias razões para isso, mas das mais comuns é que fazer e manter os testes gastam muito tempo. "Toda vez que faço uma alteração no código, os testes quebram!"

E no fim, de certa forma estão até corretas, porque testes quebrando o tempo todo é um grande empecilho para a produtividade. Entretanto, e se eu te disser que existe algumas formas de evitar essas frustrações e começar a implementar testes de interface mais confiáveis?

Quando estudamos testes, sendo pra front ou back, por muitas vezes nos preocupamos em conhecer seus fundamentos ou quais ferramentas utilizar, ou seja, tópicos que tem mais a ver com "como" implementar os testes do que "o que" realmente testar.

De acordo com Kent C. Dodds, definir muito bem "o que" testar é o ponto-chave para agregar mais valor e confiabilidade aos testes.

O conteúdo desse artigo utiliza como referência alguns textos do Dodds e está dividido em 3 partes: a primeira descreve o que não fazer ao escrever testes para o front-end; na segunda parte, veremos qual a melhor prática para escrever testes confiáveis e por fim, na última parte, veremos um pouco de como pensar os testes seguindo estrutura de código do React.

Você pode conferir as referências ao final de cada parte destes posts.

O que será abordado:

  • Porque fazer testes?
  • Detalhes de implementação
  • Falso-negativo
  • Falso-positivo

Porque fazer testes?

Em termos gerais, fazemos testes para que tenhamos sempre a confiança de que a aplicação vai funcionar como esperado quando o usuário a utilizar.

E pensando no usuário e como ele utiliza a aplicação, podemos chegar em uma metodologia que vai nos guiar a escrever testes e garantir que tragam cada vez mais confiança:

"Pensar menos sobre linhas de código e mais sobre Casos de Uso que o código atende." - Kent C. Dodds

Quando pensamos apenas sobre linhas de código ao invés de pensar em casos de uso, fica muito fácil nos perdemos e começarmos a nos preocupar em testar "detalhes de implementação", o que logo mais veremos que é uma pratica que pode gerar consequencias ruins ao desenvolvimento.

Por isso, antes mesmo de nos aprofundarmos nos testes de caso de uso que é basicamente "o que" devemos testar, vamos ver o que NÃO devemos testar na aplicação.

Detalhes de implementação

De maneira simples, podemos definir os detalhes de implementação como:

"Coisas que os usuários do seu código normalmente não usarão, verão ou sequer conhecerão." - Kent C. Dodds

E quem são os usuários do nosso código? Bom, quando escrevemos código, precisamos manter em mente que devemos atender apenas a 2 tipos de usuários:

O usuário-final: é aquele que interage com o componente (por exemplo, insere valores em inputs de um form, clica no botão enviar ou reset, recebe mensagem de carregamento e success)

O usuário-desenvolvedor: é o que renderiza o componente (por exemplo, inclui o componente em um UserProvider para atualizar seu estado e fazer dispatch pra o contexto)

Entretanto, quando fazemos testes de detalhes de implementação, acaba surgindo um terceiro usuário, que é o usuário-tester. Esse tipo de usuário não deve ter atendido, pois criando testes pra eles, faremos com que esses testes não se assemelhem à forma que a aplicação é usada pelos usuários que são importantes.

Há duas razões distintas pelas quais é importante evitar testar detalhes de implementação:

  1. Pode quebrar quando você refatora o código do aplicativo, gerando Falso-negativo.

  2. Pode acabar não falhando quando o código quebrar, gerando Falso-positivo.

Vamos para um exemplo em React, utilizando um componente accordion simples:

// accordion.js
class Accordion extends React.Component {
  state = {openIndex: 0}
  setOpenIndex = openIndex => this.setState({openIndex})

  render() {
    const {openIndex} = this.state

    return (
      <div>
        {this.props.items.map((item, index) => (
          <>
            <button onClick={() => this.setOpenIndex(index)}>
              {item.title}
            </button>

            {index === openIndex ? (
              <AccordionContents>
                {item.contents}
              </AccordionContents>
            ) : null}
          </>
        ))}
      </div>
    )
  }
}

export default Accordion
Enter fullscreen mode Exit fullscreen mode

E agora um teste utilizando a ferramenta Enzyme, para testar os detalhes de implementação:

// __tests__/accordion.enzyme.js
test('setOpenIndex sets the open index state properly', () => {
  const wrapper = mount(<Accordion items={[]} />)
  expect(wrapper.state('openIndex')).toBe(0)
  wrapper.instance().setOpenIndex(1)
  expect(wrapper.state('openIndex')).toBe(1)
})
Enter fullscreen mode Exit fullscreen mode

Ok, agora vamos entender como as coisas quebram fácil com esse tipo de teste.

Falso-negativo

Digamos que seja necessário refatorar este accordion a permitir que vários itens sejam abertos de uma só vez. Agora, vamos fazer essa alteração na implementação de modo que não altere seu comportamento:

// accordion.js
class Accordion extends React.Component {
  state = {openIndexes: [0]}
  setOpenIndex = openIndex => this.setState({openIndexes: [openIndex]})

  render() {
    const {openIndexes} = this.state

    return (
      <div>
        {this.props.items.map((item, index) => (
          <>
            <button onClick={() => this.setOpenIndex(index)}>
              {item.title}
            </button>

            {openIndexes.includes(index) ? (
              <AccordionContents>
                {item.contents}
              </AccordionContents>
            ) : null}
          </>
        ))}
      </div>
    )
  }
}

export default Accordion
Enter fullscreen mode Exit fullscreen mode

Legal! Agora a gente abre a aplicação e ve que tudo está funcionando corretamente, conforme desejamos e caso desejemos replica-lo também será muito fácil.

Aí, vamos rodar os testes eles travaram. Vemos que o teste que quebrou foi: setOpenIndex sets the open index state properly. E a mensagem de erro é a seguinte:

expect(received).toBe(expected)

Expected value to be (using ===): 0
Received: undefined
Enter fullscreen mode Exit fullscreen mode

Essa falha no teste está nos alertando sobre um problema real? Não! Pois o componente ainda está funcionando.

Isso é o que se chama de falso negativo. Isso significa que tivemos uma falha no teste, mas foi por causa de um teste quebrado, não porque o código está quebrado.

Bem, a solução então para esse caso é alterar o teste:

test('setOpenIndex sets the open index state properly', () => {
  const wrapper = mount(<Accordion items={[]} />)
  expect(wrapper.state('openIndexes')).toEqual([0])
  wrapper.instance().setOpenIndex(1)
  expect(wrapper.state('openIndexes')).toEqual([1])
})
Enter fullscreen mode Exit fullscreen mode

Chegamos então à primeira conclusão que testar detalhes de implementação podem gerar falso-negativo quando há alguma refatoração no código, o que torna os testes frágeis e frustantes que parecem falhar em simplesmente olhar o código.

Falso-positivo

Ok, agora vamos dizer que seu colega de trabalho está trabalhando no accordion e ele vê este código:

<button onClick={() => this.setOpenIndex(index)}>{item.title}</button>
Enter fullscreen mode Exit fullscreen mode

Imediatamente, pensando prematuramente em otimizar ele pensa: "Ei! Arrow functions inline são ruins para performance! Vou corrigir aqui rapidinho e fazer os testes."

<button onClick={this.setOpenIndex}>{item.title}</button>
Enter fullscreen mode Exit fullscreen mode

Aí ele faz os testes e, tcharam! Passaram! Aí sem mesmo olhar no navegador o componente funcionando, ele manda abre um PR, é aprovado e o accordion quebra em produção!

Mas o que deu errado? O teste verificava se o estado mudava quando o setOpenIndex era chamado e se o conteúdo era exibido corretamente. Sim, está correto, o problema é que não houve teste para verificar se evento onClick do botão estava chamando o setOpenIndex corretamente.

Isso é chamado de falso positivo, o que significa que o teste não falhou mas deveria ter falhado! Então, o que podemos fazer para garantir que não ocorra novamente?

Poderíamos pensar em algumas ações como: adicionar outro teste para verificar se clicar no botão atualiza o estado corretamente ou simplesmente usar uma ferramenta gratuita que nos auxilia a evitar testar detalhes de implementação.

Eu gosto dessa segunda alternativa, aposto que você também! Veremos esses detalhes na parte 3 em que falamos sobre os testes no React!

Mas por enquanto, vamos seguir a linha de raciocínio e aprender sobre os testes que realmente devemos nos preocupar em escrever: "Testes de caso de uso".

👉 Continue lendo na parte 2


Referências:
"How to know what to test" - Kent C. Dodds
https://kentcdodds.com/blog/how-to-know-what-to-test
"Avoid the Test User" - Kent C. Dodds
https://kentcdodds.com/blog/avoid-the-test-user
"Testing Implementation Details" - Kent C. Dodds
https://kentcdodds.com/blog/testing-implementation-details

Top comments (0)