Mantendo os Testes Valiosos: Pense nos Inputs!
Para que os testes de unidade sejam realmente eficazes, é essencial pensar nas entradas!
Neste artigo, discutiremos por que é tão importante evitar entradas incorretas, pensar em entradas mais amplas em testes de unidade e como essa prática pode afetar diretamente os resultados do teste. Vamos entender isso com um exemplo. Espero que o artigo seja útil!
📍Precisamos pensar como o usuário
O exemplo que vamos comentar demonstra a importância de pensar em outros cenários e entradas para testes de unidade. Dessa forma podemos garantir um conjunto mais robusto de testes que se preocupam com o comportamento da funcionalidade de maneira mais ampla.
Suponha que temos um método IsPalindrome()
que verifica se uma string é um palíndromo (se ela pode ser lida tanto da esquerda para a direita quanto da direita para a esquerda e ainda ser a mesma string). Sabemos que então a palavra Level
na lingua inglesa é um palíndromo. Então vamos escrever o código e depois o teste:
Olhe atentamente para este código e veja se você descobre o seu problema. Vou colar aqui o teste de unidade para este método:
Aparentemente tudo bem. Se você executar este teste, tudo será executado com sucesso. Mas e se o desenvolvedor inserir essa entrada abaixo?
var result = IsPalindrome("LeveL");
O desenvolvedor executa o teste e tudo passa com sucesso! Legal, então podemos enviar o código para produção e nos livrar dessa tarefa e puxar outra porque é isso que seu gerente quer, certo?
Observe que aqui só testamos dois inputs diferentes, mesmo que esses inputs sejam válidos e façam sentido para o contexto e o teste, você não acha que são poucos cenários explorados? Bom vamos ver como termina essa história.
Depois de algumas horas, um usuário reclama que a palavra leveL
não é reconhecida como um palíndromo. O desenvolvedor que escreveu o recurso se assusta e pensa; "Como isso é possível? Até fiz os testes de unidade!".
Por mais hipotética que seja essa situação, ela acontece com bastante frequência. O problema aqui é que para o olho humano, essas variações da mesma palavra abaixo são a "mesma coisa" e tem o mesmo significado:
"Level", "LeveL", "leveL", "level" // differents inputs
Mas para uma máquina, existem diferenças. A palavra escrita leveL
é a mesma que o string level para um ser humano?
Para um não programador, pode ser, mas quem tem conhecimento de programação sabe que as linguagens de programação podem fazer distinções entre essas duas strings. Então, o que acontece se o input leveL
for inserido no teste? Dê uma olhada na foto:
O teste falha porque não transforma as strings em input.ToLower()
. Isso indica que há uma falha no processo de conversão do nosso código de implementação, deveríamos ter mudado a entrada para minúsculas antes de comparar as strings. Mas lembre-se, nunca devemos fazer isso em testes. Quem deve realizar essa lógica é a unidade do código em teste. A imagem abaixo deixa claro o que nunca devemos fazer!
O teste de unidade provou que a seção Act precisa ser revisada, o código que está sendo testado não converte corretamente todas as entradas inseridas! Então é por isso que devemos ir mais longe em nossos testes e pensar com clareza no que o bloco de execução do Act fará, devemos sempre pensar:
Passar esta entrada faz sentido?
Um input com uma lista vazia faria a unidade de código em teste retornar o que eu esperava?
O incentivo aqui é pensar além dos caminhos felizes e não se contentar com os poucos resultados gerados.
A regra de negócio, neste caso, não se importa se as letras são maiúsculas, para o usuário a palavra leveL
e level
têm o mesmo significado, independente de serem maiúsculas ou não, o mesmo vale para as demais palavras.
Portanto, é por isso que ignoramos caracteres maiúsculos e, ao receber as informações, a entrada é convertida em uma string minúscula. Vou deixar o código refatorado, e se você quiser testar, verá que com a string leveL
, agora o teste vai passar!
Espero que este exemplo tenha ajudado você a visualizar a importância de verificar os inputs e pensar cuidadosamente sobre a passagem deles para o bloco de Ação. É sempre importante verificar se as entradas (inputs) do código em teste estão corretas. Lembre-se de nunca colocar a lógica de domínio em testes. Os testes unitários precisam saber o mínimo possível do código em teste!
Outro ponto importante a ser observado, o desenvolvedor não pensou se a lógica poderia estar incorreta, pois não planejou outros casos de teste com entradas diferentes.
Lembre-se:
Ter testes com valores de entradas precipitadas pode levar os desenvolvedores a usar valores que não correspondem aos casos de uso reais, o que pode levar a resultados enganosos ou falhas de teste.
Agora para finalizar este tópico, vou deixar uma imagem com um teste onde consideramos vários cenários com strings diferentes. Obviamente após a correção no código, todos os testes devem passar:
Reforço novamente a sempre questionar o código e seu comportamento, como se fosse um usuário. Pense em como um usuário poderia inserir um valor e como seria o fluxo. Qual efeito isso teria? Tenho validações para essa entrada? Essas perguntas podem auxiliar no processo de planejamento e escrita dos testes!
📍 Efeitos nocivos de testes com entradas incorretas
Entradas incorretas em testes de unidade podem afetar negativamente a qualidade dos testes e do software de várias maneiras, podemos listar várias:
Falsos negativos: Testes com entradas inadequadas podem passar, mesmo quando há problemas com o código. Isso pode resultar em bugs não detectados que afetam negativamente a qualidade do software e podem causar problemas quando o software é lançado em produção.
Falsos positivos: Testes com entradas incorretas ou implementação de código exposto podem falhar, mesmo quando o código está funcionando corretamente. Isso pode levar ao desperdício de tempo e esforço tentando resolver problemas que não existem, atrasando o desenvolvimento e a entrega do software.
Casos de teste descobertos: Testes de unidade com ou inputs inválidos podem não cobrir todos os caminhos no código, deixando partes do código não testadas.
Dificuldade na manutenção do código: Entradas incorretas podem tornar o código mais difícil de manter. Quando o código muda, os testes podem começar a falhar por causa de suas entradas incoerentes, tornando mais difícil identificar se as falhas são causadas por problemas reais no código ou apenas pelos testes com inputs que não agregam valor ao teste.
Baixa confiabilidade: Um conjunto de testes de unidade com entradas incorretas pode minar a confiança no conjunto de testes como um todo. Isso pode levar a menos confiança na qualidade do código e aumentar o risco de problemas não detectados na produção.
Vamos falar rapidamente sobre os benefícios!
📍 Benefícios de testes com entradas estáveis e confiáveis
Torna-se evidente que quando elaboramos testes com maior cuidado e evitamos a simples inserção de informações inconsistentes, obtemos inúmeros benefícios para o desenvolvimento e manutenção do software, podemos enumerar:
Refatoração e manutenção seguras: Com testes unitários de alta qualidade, os desenvolvedores podem modificar o código e garantir que as mudanças não interrompam a funcionalidade existente. Isto simplifica a refatoração e a manutenção do código ao longo do tempo.
Identificação precoce de problemas: Testes precisos e bem organizados podem detectar erros e problemas no código antes que eles sejam levados à produção, economizando tempo e esforço na solução de problemas posteriormente.
Torna a incorporação de novas funcionalidades mais fácil: Um conjunto de testes correto e consistente garante que as novas funcionalidades se integrem adequadamente ao código existente, minimizando as dificuldades de compatibilidade e garantindo que as funcionalidades atuem como pretendido.
Aumenta a confiabilidade do software: Testes unitários confiáveis e consistentes aumentam a confiabilidade do software, assegurando que os requisitos sejam atendidos e que o software opere adequadamente em diferentes cenários.
Redução de riscos: Testes com entradas válidas e abrangentes contribuem para reduzir o risco de falhas de software e problemas de segurança, protegendo os usuários e a reputação da empresa.
Evita regressões: Ao pensar nos inputs de cada cenário de teste, você cria um conjunto abrangente de testes que pode ser usado para verificar se futuras alterações no código não introduzem novos erros ou quebrem a funcionalidade existente.
Facilita a manutenção: Um conjunto de testes unitários bem projetado com diferentes cenários e inputs facilita a manutenção do código. Quando os desenvolvedores precisam fazer alterações ou correções, os testes fornecem uma maneira rápida e confiável de verificar se as mudanças não afetaram negativamente outras partes do software.
📍Depois de escrever os testes, pergunte a si mesmo!
No tópico anterior, vimos os benefícios. Estou deixando algumas dicas de perguntas a serem feitas para que cada um de nós pense depois de escrever um teste, para que possamos detectar entradas incorretas nos testes unitários:
O teste examina funcionalidade ou comportamento específicos? Os testes unitários devem ser focados e avaliar apenas uma característica de cada vez.
As entradas usadas nos testes são realistas e refletem os cenários de uso do software? Mantenha as informações de entrada que representam casos de uso reais, limites e possíveis variações.
A lógica do teste é compreensível e fácil de seguir? Certifique-se de que a lógica do teste seja simples, objetiva e devidamente documentada, facilitando sua compreensão e manutenção no futuro.
Os testes verificam as condições apropriadas? Confirme que o teste está realmente medindo o comportamento esperado, busque verificar se as entradas realmente fazem sentido para o contexto e requisitos.
O teste é responsável por cenários de falha ou exceções? Certifique-se de que os testes abordam casos onde podem surgir erros ou exceções, assegurando que o software lida adequadamente com situações imprevistas.
A cobertura do código é adequada? Use ferramentas de análise de cobertura de código para garantir que seus testes cubram todos os caminhos de código relevantes.
Os testes são independentes uns dos outros? Cada teste unitário deve ser independente e nunca depender dos resultados de outros testes, garantindo que eles possam ser realizados em qualquer seqüência e focados no comportamento observável.
Existe redundância ou repetição entre os testes? Certifique-se de que seus testes não estejam examinando o mesmo comportamento várias vezes, o que pode tornar seu conjunto de testes menos eficaz e mais complexo de manter.
📍Conclusão
Em conclusão, evitar inputs inválidos e buscar refinar essas entradas de dados nos testes, é fundamental para garantir a qualidade do software, confiabilidade e capacidade de manutenção.
Ao dedicar tempo e atenção as entradas dos testes, à criação e revisão de testes unitários, você está investindo o sucesso a longo prazo de seu projeto. Lembre-se de questionar e analisar criticamente todos os aspectos de seus testes!
Eu o encorajo a compartilhar conhecimentos, experiências e lições aprendidas que podem levar a um ambiente de trabalho mais produtivo e eficiente, onde todos estão comprometidos em produzir software de alta qualidade.
Espero que este post tenha ajudado você, se você gostou, por favor, compartilhe com outras pessoas! Até o próximo post! 😉😄
Livros que eu considero essenciais para todos ✅:
Effective Software Testing: A Developer's Guide - Mauricio Aniche
Unit Testing Principles, Practices, and Patterns - Vladimir Khorikov