Mantendo os Testes Valiosos: Evite Falsos Negativos!
A prevenção de falsos negativos em testes de unidade é crucial para garantir a confiabilidade e eficácia do software.
Continuando nossa série, no último post falamos sobre regressões de funcionalidades e o importante papel dos testes de unidade no combate a essas regressões. O assunto ainda continua sobre a criação de testes que evitam regressões, mas vamos focar em um grande perigo que devemos sempre estar atentos e evitar, os testes que geram falsos negativos.
Vamos entender o que são falsos negativos, os malefícios que eles trazem para a confiabilidade e qualidade do software, como evitá-los e dicas de como criar testes que evitem falsos negativos.
📍Cuidado com falsos negativos!
Falso Negativo significa que um caso de teste passa enquanto o software contém o bug que o teste pretendia capturar.1
Um falso negativo indica que não há bug quando há um.
Tá, mas como isso acontece? Deixe-me tentar demonstrar um exemplo com C#. Observe que é um exemplo didático para tentar ilustrar como podemos identificar um falso negativo. Temos no exemplo uma classe responsável por verificar se um usuário pode acessar um recurso específico com base em sua idade, país e assinatura premium. Vamos supor que o desenvolvedor aplicou uma refatoração no código descrito na imagem a seguir:
Antes de mostrar a refatoração, observe que o desenvolvedor não prestou atenção e acabou introduzindo o operador lógico errado:
Agora confira o caso de teste que foi escrito sem planejamento e com pouca atenção:
Neste exemplo, o teste verifica se um usuário maior de 19 anos, natural do Brasil e sem assinatura premium pode acessar o recurso. O teste passará porque a condição será verdadeira:
age >= AgeMinimum && country == "Brazil" || hasPremiumSubscription
O operador lógico "OR" (||) utilizado não é adequado para esse caso, ele é utilizado para combinar duas expressões booleanas e retorna verdadeiro se pelo menos uma das expressões for verdadeira, ou seja, é incorreto utilizar este operador no retorno para este contexto.
Mas a real exigência é que o usuário tenha idade mínima, more no Brasil e tenha uma assinatura premium (todos os critérios devem ser verdadeiros) para acessar o recurso. Nesse caso, o teste não revela o problema no código e dá um falso negativo. O requisito pode ter sido entendido da maneira certa (ou talvez não), mas por algum motivo, o desenvolvedor usou o operador lógico errado.
Pode ser que o desenvolvedor não estava focado o suficiente ou estava com pressa para entregar a tarefa e, portanto, escreveu este teste de unidade sem sentido. Isso demonstra um ponto importante sobre como devemos pensar e escrever vários cenários diferentes em testes. Se o desenvolvedor e a equipe acharem que esse teste é suficiente, ou mesmo não fizerem uma revisão de código, um bug será repassado.
O que ajudaria a detectar esse falso negativo? Escrevendo outros casos de teste! Portanto não podemos nos contentar apenas com saídas verdadeiras (afirmações que esperam apenas true), devemos escrever testes com saídas que considerem um resultado esperado como falso, veja o exemplo abaixo:
O teste acima foi escrito para passar (ficar verde ✅) pois esperamos o retorno como falso, afinal a entrada USA
não é válida de acordo com os requisitos.
Mas sabemos que o teste vai ficar vermelho e não vai passar (🛑). Então agora estamos considerando outras entradas e outros casos de teste e isso traz mais confiança ao nosso conjunto de testes:
De acordo com a implementação atual da classe após a refatoração que alterou o comportamento, a expressão age >= AgeMinimum && country == "Brazil" || signaturePremium
retorna verdadeiro mesmo que o usuário more em outro país diferente do Brasil. Não é essa a regra correta!
O novo teste que acabamos de escrever com "USA
" como país de entrada, faz com que o método UserAccessResource() retorne true mesmo se o usuário não atender a todos os critérios (idade mínima, morar no Brasil e ter assinatura premium). Afinal, a lógica está incorreta.
Desta forma o teste irá falhar 🛑, pois é esperado que o método retorne false nesta situação, declaramos um Assert.False
, que vai receber true
como resultado em vez de uma afirmação false
. Assim, com este teste, poderemos capturar qualquer regressão na funcionalidade!
Mas qual a relação entre regressão e testes que geram falsos negativos? A relação está na capacidade dos testes em detectar problemas introduzidos por novas mudanças no código. Se os testes automatizados não conseguem identificar uma regressão de funcionalidade porque sempre geram falsos negativos, isso pode levar ao aumento do número de bugs e problemas no software.
Além disso, isso não cria confiança no software e nos testes! Não está claro? Podemos fazer outra analogia. Por favor, seja paciente comigo 😂.
Vamos separar a analogia em duas etapas.
Imagine que você é o técnico de um time. Sua equipe está jogando bem e todas as posições funcionam, seus jogadores se dão bem e têm uma boa harmonia. Então, para dar chance aos outros jogadores reservas, você decide fazer uma mudança no esquema tático para deixar o time mais afinado, adicionando um novo jogador e ajustando a posição de alguns outros. A regressão da funcionalidade seria como se, após essa mudança de posições de jogadores, uma parte do seu time começasse a jogar pior do que antes, pode ser o meio-campo ou até mesmo o ataque. Talvez os jogadores não estejam se dando bem, ou o novo jogador não esteja se encaixando bem no esquema tático. Parece que houve uma regressão no desempenho de sua equipe, podemos dizer que equivale a uma regressão de funcionalidade no desenvolvimento de software.
Mas e os falsos negativos? Ainda usando o exemplo do time de futebol, pense que você realiza um treinamento específico para testar a eficácia do seu novo esquema tático. Durante o treinamento, tudo parece estar funcionando perfeitamente. No entanto, quando chega a hora do jogo oficial, seu time não tem um desempenho tão bom quanto nos treinos, e as lacunas aparecem. Parece que os treinamentos falharam em detectar essas falhas, ou seja, houve falsos negativos.
Assim como o treinador deve identificar falhas no desempenho da equipe e corrigir as estratégias, nós, como engenheiros de software, devemos garantir que o teste seja eficaz na identificação de regressões de funcionalidades.
Se o teste não conseguir detectar essas regressões porque sempre há falsos negativos, a qualidade do software será prejudicada. Os falsos negativos permitem que as regressões de funcionalidade não sejam detectadas, pois os testes de unidade não estão capturando os problemas presentes no código.
Ok, você já falou o suficiente sobre os problemas, e a solução? Posso listar algumas dicas, que podem ser possíveis soluções, espero que sejam úteis:
Planeje e escreva testes abrangentes e eficientes, garantindo que funcionalidades críticas e casos extremos (principalmente as fronteiras de regras) sejam testados. Pare de escrever testes com pressa, pare de escrever testes sem planejamento, isso não te beneficia.
Atualize os testes à medida que o software evolui e sua complexidade aumenta para que reflitam as alterações no código. Ter testes que cobrem apenas pequenas partes do comportamento, mas não testam os principais recursos e funcionalidades críticas são praticamente inúteis.
Realize testes de regressão com frequência, para garantir que novas alterações não afetem negativamente o desempenho do sistema.
Envolva o controle de qualidade quando surgirem muitas dúvidas. A equipe de QA's geralmente são especialistas no domínio. Eles conhecem muitas regras e estão em contato constante com o cliente.
Implemente processos de revisão de código e integração contínua, para identificar e corrigir bugs o mais cedo possível. A revisão de código é essencial, onde outros desenvolvedores podem dar sugestões e apontar falhas, tanto nos testes unitários quanto no próprio código implementado.
Não crie testes que esperem apenas casos positivos ou retornos verdadeiros como resultados finais, ou seja,
Asserts.True
. Teste se o comportamento também falha e retorna falso quando a regra não é cumprida e não está de acordo com os requisitos!
É papel de todos os desenvolvedores colaborar para escrever testes que detectam regressões! Todos nós precisamos garantir que os testes sejam eficazes e evitem falsos negativos.
📍Como os falsos negativos quebram a confiança do cliente ou usuário no software?
Vamos contar uma pequena história. A Joana é uma profissional independente que utiliza uma aplicação de faturação chamada InvoicePro para gerir as suas faturas e pagamentos. Recentemente, a equipe de desenvolvimento do InvoicePro lançou uma atualização com novos recursos e melhorias na interface.
No entanto, um falso negativo em um dos testes de unidade deixou passar uma regressão na funcionalidade de cálculo de impostos. O conjunto de testes verificou que o imposto foi calculado corretamente, mas não detectou um cenário específico em que o cálculo falhou para taxas de imposto personalizadas.
Após a atualização, Joana cria uma nova fatura para um cliente e insere uma taxa de imposto personalizada. Ao revisar a nota fiscal antes de enviá-la, ela percebe que o cálculo do imposto está incorreto. Joana tenta resolver o problema, mas não encontra solução.
Preocupada em perder a credibilidade com o cliente, Joana decide calcular o imposto manualmente e editar a fatura antes de enviá-la. Isso consome um tempo valioso e frustra Joana, que contava com o aplicativo para fazer esses cálculos automaticamente.
A experiência negativa leva Joana a questionar a fiabilidade da InvoicePro e a avaliar a procura de alternativas. A história acima, exemplifica como um falso negativo em testes unitários pode resultar em falhas de software que impactam diretamente o usuário final e comprometem a credibilidade do cliente no produto.
Que lição podemos aprender com essa história? Vou listar abaixo 3 lições que considero essenciais:
A importância de testes de unidade eficazes: Testes de unidade que não detectam problemas no código, gerando falsos negativos, podem permitir que erros passem despercebidos e afetem a funcionalidade do software. É crucial garantir que os testes sejam bem escritos e cubram todos os cenários e casos de uso possíveis.
O impacto da confiança do cliente: quando o software apresenta problemas que afetam diretamente o usuário final, a confiança do cliente no produto é prejudicada. Experiências negativas podem levar os clientes a buscar soluções alternativas e afetar a reputação do produto no mercado.
A necessidade de monitoramento e suporte pós-lançamento: Mesmo com testes extensivos, é importante monitorar o comportamento do software em produção e fornecer suporte eficiente aos usuários. Identificar e corrigir problemas rapidamente pode ajudar a minimizar os impactos negativos na experiência do usuário e na confiança do produto.
Podemos concluir que falsos negativos em testes unitários são graves, pois podem causar diversos problemas e impactos negativos no desenvolvimento e na experiência do usuário. É crucial que as equipes de desenvolvimento de software se esforcem para garantir que os testes de unidade sejam eficazes e abrangentes, minimizando a possibilidade de problemas não detectados.
📍Os falsos negativos afetam os prazos?
Com base em tudo o que lemos, podemos dizer que sim. Principalmente porque é sempre necessário dedicar tempo na investigação do problema, tanto para o código quanto para os testes!
Não faz sentido resolver a regressão de funcionalidade, mas permitir que o falso negativo mantenham-se em seu conjunto de testes. Fique alerta! Podemos listar os diversos aspectos que levam a equipe de desenvolvimento a perder prazos por causa de falsos negativos em testes:
Interrupção do fluxo de trabalho: quando um falso negativo é descoberto, os desenvolvedores podem precisar interromper o trabalho em que estão envolvidos no momento para corrigir o problema. Essa mudança de foco pode prejudicar a produtividade e o planejamento geral do projeto.
Retrabalho: falsos negativos podem resultar em retrabalho, pois os desenvolvedores precisam revisar o código que achavam que estava funcionando corretamente. Esse retrabalho consome tempo e recursos valiosos que poderiam ser usados em outras partes do projeto.
Diagnóstico tardio de um problema: como os falsos negativos indicam que um teste foi aprovado mesmo quando há um problema no código, os desenvolvedores podem não perceber a existência de um problema até que seja tarde demais. Isso pode resultar em um diagnóstico tardio, o que pode levar a mais tempo sendo gasto investigando e resolvendo o problema.
Elevação do custo de reparo: à medida que o tempo passa sem a identificação de um problema, o custo para solucioná-lo tende a aumentar. Essa situação se deve ao acúmulo de modificações no código, o que pode tornar a resolução do problema mais intrincada.
O que listamos acima, geralmente já enfrentamos! Mas precisamos continuar nesse ciclo vicioso? Não! Podemos escrever testes que transmitem confiança e falham apenas quando necessário! Mas, para isso, todos devem estar cientes de que é preciso dedicação para alcançar um conjunto de testes seguro e eficaz!
📍 Conclusão
Em conclusão, falsos negativos em testes de unidade representam um problema significativo no desenvolvimento de software, pois podem levar à introdução de erros e problemas de qualidade no produto final.
Para enfrentar esse desafio, as equipes de desenvolvimento devem adotar boas práticas de teste, promover revisões e emparelhamento de código e investir em treinamento e conscientização sobre a importância de testes de unidade eficazes.
Muito obrigado por ler até o final, espero que as dicas possam ajudar! Por favor, curta e compartilhe! Até o próximo artigo! 😄🖐️
Livros que considero essenciais para todos ✅:
Effective Software Testing: A Developer's Guide - by Mauricio Aniche
Unit Testing Principles, Practices, and Patterns - by Vladimir Khorikov
A expressão “falso negativo” é usada para descrever uma situação em que um teste falha em detectar um problema existente. No contexto dos testes de software, um falso negativo ocorre quando um caso de teste passa (indica que o software está funcionando corretamente), mas na realidade o software contém um bug que o teste deveria ter capturado.
Por que “falso negativo”?
A terminologia vem da estatística e é amplamente utilizada em áreas como testes médicos e de diagnóstico.
• “Negativo”: Significa que o teste não detectou um problema (indicou que tudo está normal).
• “Falso”: Indica que essa conclusão está errada.
Portanto, um falso negativo em um teste de unidade significa que o teste mostrou que não havia erro (um resultado “negativo”), mas essa conclusão estava errada porque o bug realmente existia (daí ser “falso”).
Exemplo prático:
Imagine que você escreveu um teste de unidade para verificar se uma função de cálculo de desconto está funcionando corretamente. A função, no entanto, tem um bug que não aplica o desconto em determinadas situações. Se o teste de unidade não for abrangente ou específico o suficiente para capturar esse caso, ele pode passar e indicar que a função está correta, mesmo com o bug presente. Esse é um falso negativo: o teste passou, mas o bug estava lá.
Por que isso é um problema?
Falsos negativos são prejudiciais porque:
• Criam uma falsa sensação de segurança: Os desenvolvedores acreditam que o código está funcionando corretamente, quando na verdade não está.
• Podem levar a falhas em produção: Bugs não detectados nos testes podem se manifestar em situações reais, causando problemas para os usuários e a empresa.
• Dificultam a confiança nos testes: Se os testes de unidade frequentemente apresentam falsos negativos, os desenvolvedores começam a duvidar da eficácia da suíte de testes.
Portanto, é fundamental que os testes de unidade sejam bem projetados para evitar falsos negativos e garantir que eles detectem os problemas quando realmente existirem.