Índice
Saber testar é importante e ótimo. O Kent criou diversos artigos ensinando os fundamentos dos testes, como configurar ferramentas, como escrever testes para cenários específicos, e muito mais. Mas, saber como escrever testes é apenas metade da batalha para ganhar confiança na sua aplicação. Saber o que testar é a outra metade importante.
Nos materiais de Workshop e no testingjavascript.com, Kent explica sobre como saber o que testar, porém, é questionado diversas vezes sobre este assunto. Então, ele decidiu abordar este tema, traduzido nesta postagem.
Relembrando porquê nós testamos
Nós escrevemos testes para estarmos confiantes de que nossa aplicação irá funcionar quando o usuário precisar utilizá-la. No entanto, algumas pessoas escrevem testes para melhorar o fluxo de trabalho e isso é ótimo, porém, ultimamente, estou interessado em confiança. Sendo este o caso, o que testamos deve ser mapeado para aumentar nossa confiança. Este é o ponto que quero que você considere quando escrever testes:
Pense menos sobre o código que você está testando e mais nos casos de uso que seu código suporta.
Quando você pensa no código em si, é muito fácil e natural começar a testar detalhes de implementação (o que é um caminho para o desastre).
Pensar em casos de uso nos aproxima de escrever testes da maneira que o usuário usa a aplicação.
Quanto mais seu teste assemelha-se da forma como o software é utilizado, mais confiança ele pode dar à você. Kent C. Dodds 🧢 (@kentcdodds) March 23, 2018.
Código Coverage < Caso de uso Coverage
Cobertura de código (code coverage) é uma métrica que nos mostra quais linhas de nosso código estão sendo executadas durante os testes. Vamos usar o código abaixo como exemplo:
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
Neste momento, nós não temos testes para essa função, então, nossa cobertura de código indica que temos 0% de cobertura dessa função. O resultado de cobertura de código, nesse caso, nos ajuda com a ideia de que testes são necessários. Mas, isso não nos diz o que é importante sobre esta função, nem ao menos os casos de uso que a mesma suporta, que é a consideração mais importante para termos em mente quando escrevemos testes.
Em fato, quando consideramos um aplicativo inteiro e pensamos sobre o que testar, a cobertura de código nos retorna um relatório bem pobre sobre onde devemos investir o nosso tempo.
O relatório de cobertura de código nos ajuda a identificar quais códigos em nossa base estão sem testes. Então, quando você olhar para um relatório de cobertura de código e encontrar linhas que estão sem testes, não pense em if/elses, loops, ou lifecycles. Ao invés disso pergunte à você mesmo:
Quais casos de uso essas linhas de código suportam e que testes posso criar para dar suporte a esses casos de uso?
Caso de Uso Coverage nos informa quantos casos de uso nossos testes suportam. Infelizmente, não há algo que possa nos fornecer automaticamente um relatório de “Casos de Uso Coverage”. Nós mesmos precisamos fazer isso. Mas o relatório de cobertura de código (code coverage), às vezes, pode nos ajudar a identificar casos de uso que não estamos cobrindo. Vamos tentar isso.
Se lermos o código e considerarmos por um minuto, podemos identificar nosso primeiro suporte ao caso de uso: “Isto deve retornar uma array se for fornecido uma array”. Esse caso de uso é um bom título para o nosso teste.
test('retorne uma array se for fornecido uma array', () => {
expect(arrayify(['Elephant', 'Giraffe'])).toEqual(['Elephant', 'Giraffe'])
})
E com esse teste, nosso relatório de cobertura se parece com isso (as 3 primeiras linhas são cobertas):
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
Agora, podemos olhar para as linhas restantes e determinar que existem mais dois casos de uso que o nosso teste não suporta ainda:
- Retorna uma array vazia se for fornecido um valor falso;
- Retorna uma array com o argumento fornecido se não for uma array ou falso.
Vamos adicionar testes para esses casos de uso e ver como isso afeta a cobertura de código.
test('retorne uma array vazia se for falso', () => {
expect(arrayify()).toEqual([])
})
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
Ótimo, quase lá!
test(`retorne uma array com o argumento fornecido se o argumento não for uma array ou falso`, () => {
expect(arrayify('Leopard')).toEqual(['Leopard'])
})
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
Legal! Agora podemos ter certeza que, desde que não precisemos mudar os casos de uso dessa função, nossos testes continuarão passando.
Cobertura de código não é uma métrica perfeita, mas pode ser uma ferramenta útil para identificar que partes de nossa base de código estão sem “cobertura de caso de uso”.
Cobertura de código pode esconder caso de uso
Às vezes, nosso relatório de cobertura de código pode indicar 100% de cobertura, mas, não ter 100% de cobertura de caso de uso. É por isso que, às vezes, eu tento pensar em todos os casos de uso antes de começar a escrever testes.
Por exemplo, vamos imaginar que a função arrayify
foi implementada dessa maneira:
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else {
return [maybeArray].filter(Boolean)
}
}
Sendo assim, nós podemos conseguir 100% de cobertura de código com os dois casos seguintes:
- Retorna uma array se for fornecido uma array;
- Retorna uma array se o argumento fornecido não for uma array.
Mas, se pudéssemos olhar para um relatório de cobertura de casos de uso, veríamos que estamos deixando passar um caso de uso:
- Retorna uma array vazia se for fornecido um valor falso.
Isso pode ser ruim, porque agora os nossos testes não estão nos fornecendo muita confiança de que o nosso código funcionará quando os usuários o usarem assim: arrayify()
. No momento, tudo bem, porque embora não tenhamos um teste, nosso código suporta esse caso de uso. Mas, o motivo de termos testes é garantir que o código continue suportando os casos de uso, mesmo quando as coisas mudarem.
Então, um exemplo de como a falta desse teste pode dar errado, alguém pode ver este código .filter(Boolean)
e pensar: “Uhm, isso é estranho… Será que realmente precisamos disso?”. Então ele remove isso, e nossos testes continuam passando. Porém, qualquer código que se baseou no comportamento falso estará quebrado.
Lembre-se: Teste casos de uso, não código.
Como isto se aplica ao React?
Testar no front-end pode ser um pouco diferente das práticas aplicadas no back-end, por mais que a teoria siga o mesmo princípio.
Continuando…
Quando estiver escrevendo código, lembre-se que você tem dois usuários que precisa suportar: Usuários finais e pessoas desenvolvedoras. Novamente, se você pensar no código ao invés de pensar nos casos de uso, torna-se perigoso e natural começar a testar detalhes de implementação. Quando você faz isso, seu código ganha um terceiro usuário.
Existem alguns aspectos do React que as pessoas pensam em testar e que acabam resultando em testes de detalhe de implementação. Para todos esses, ao invés de pensar sobre o código, pense sobre o efeito observável que esse código tem para o usuário final e a pessoa desenvolvedora, esse é seu caso de uso, teste ele.
- Métodos de Lifecycle
- Elementos de event handler
- Estado interno do componente
Por outro lado, aqui estão algumas coisas que você deve testar porque elas atendem aos seus dois usuários. Cada um deles podem mudar o DOM, fazer requisições HTTP, fazer uma chamada callback, ou executar vários outros efeitos colaterais observáveis que podem ser úteis no teste.
- Interação de usuário (usando
fireEvent
do React Testing Library): O usuário final é capaz de interagir com o elemento que este componente renderiza? - Mudandoprops (usando rerenderdo React Testing Library): O que acontece quando a pessoa desenvolvedora renderiza novamente seu componente com novas props?
- Mudança de contexto (usando rerender do React Testing Library): O que acontece quando a pessoa desenvolvedora muda o contexto renderizando novamente seu componente?
- Mudanças de assinaturas: O que acontece quando um emissor de eventos muda as assinaturas do componente? (Como Firebase, uma store do Redux, uma rota, um media query ou uma assinatura baseada na DOM como status de online)
Como eu sei por onde começar em uma aplicação?
Então nós sabemos como pensar sobre “o que testar” para componentes individuais e até para páginas de sua aplicação, mas, por onde começamos? É um pouco estressante. Especialmente se você está começando a testar uma aplicação grande.
Então, considere a aplicação do ponto de vista do usuário e pergunte:
Qual parte desta aplicação me deixaria mais chateado se não estivesse funcionando?
De outro modo, e mais comum:
Qual seria a pior coisa para quebrar nessa aplicação?
Eu recomendo que você faça uma lista das funcionalidades que sua aplicação suporta e as priorize com base nesses critérios. É um ótimo exercício para fazer com o seu time e gestor(a). Este encontro pode ter um efeito colateral, ajudando todos na sala a entender a importância de testar e, talvez, até os convença que devem priorizar os testes em todos os outros recursos que forem desenvolvidos.
Quando você estiver com a lista de itens prioritários, sugiro que escreva um teste end to end (E2E) para cobrir o “caminho feliz” que a maioria dos usuários irá passar em um caso específico. Normalmente, você pode cobrir a maioria das funcionalidades de sua lista dessa maneira. Isso pode levar um pouco de tempo para ser configurado, porém lhe dará um GRANDE retorno econômico.
Os testes E2E não irão lhe garantir 100% de cobertura dos casos de uso (e você nem deve tentar cobrir 100%), muito menos fornecerão 100% de cobertura de código (e você também nem deve se importar com isso em testes E2E), mas fornecerá um ótimo ponto de partida e aumentará muito a sua confiança.
Assim que tiver alguns testes E2E, você pode começar a escrever testes de integração para casos que você está deixando passar em seus testes E2E, além de testes de unidade para a lógica de negócios mais complexa que esses recursos estão usando. A partir daqui, você adicionará testes ao longo do tempo. Só não se preocupe em focar no 100% code coverage, não vale a pena.
➕ Para estabelecer uma cultura de testes e metas de cobertura de código razoáveis, sugiro que assista a palestra do Aaron Abramov – Establishing testing patterns with software design principles
📚 Leia mais sobre as diferenças entre os tipos de teste aqui: Static vs Unit vs Integration vs E2E Testing for Frontend Apps
Conclusão
Com tempo e experiência suficientes, você desenvolverá uma intuição para saber o que deve testar. Você provavelmente cometerá erros e sofrerá um pouco. Não desista! Continue. Boa sorte.
💡 Este post é uma tradução do artigo “How to known what to test”, do Kent C. Dodds.