Mantendo os Testes Valiosos: Cuidado com as Regressões!
Quanto mais eficazes forem os testes, mais segurança e confiança temos na luta contra as regressões!
Escrever testes é uma tarefa que requer nossa atenção para manter a qualidade do software! Neste artigo vamos falar sobre regressões, entender seus perigos e como elas costumam ocorrer, como identificar possíveis regressões, apresentar alguns exemplos práticos e comentar os benefícios de escrever testes que protegem o software contra regressões.
Vamos começar entendendo melhor o que é regressão no contexto da engenharia de software.
📍O que é regressão?
Gosto sempre, antes de estudar qualquer tema, entender as origens da palavra. Normalmente começo entendendo o significado da palavra, que neste caso é regressão. A palavra vem do latim "regressus”, que significa “voltar a um estado ou lugar anterior”. Pesquisando mais a fundo a palavra no dicionário, encontrei algumas referências interessantes que vou listar abaixo:
uma situação em que as coisas pioram em vez de melhorar.
Quem nunca passou por uma situação dessas? Seja em aspectos pessoais da vida ou no trabalho? Agora veja outra descrição interessante para a palavra regressão:
um retorno a um estado, condição ou comportamento anterior e menos avançado ou pior.
Normalmente, quando precisamos levar nosso carro que está com algum problema mecânico para ser analisado e consertado, sempre esperamos que o problema seja resolvido e o carro volte ao estado correto e melhor. Nunca queremos que o estado ou condição do carro volte pior ou com novos problemas! Isso vale também para o contexto da engenharia de software! É importante entender que uma regressão no software significa que uma funcionalidade deixou de funcionar conforme planejado após alguma modificação no código, geralmente após a implementação de uma nova funcionalidade. E isso é algo muito sério! Geralmente, regressões são temidas por todos, principalmente em grandes corporações onde o impacto de uma função importante parar de funcionar pode gerar prejuízos de milhões! E existem dois tipos de regressão: funcional e não funcional. Veja as diferenças:
Funcional: é a forma mais comum que vemos e ocorre quando uma alteração no código leva à quebra de uma funcionalidade existente, que faz parte da solução de negócios existente.
Não funcional: é uma forma menos comum de regressão e ocorre quando uma alteração no código leva a uma degradação de aspectos não funcionais do software, como desempenho, escalabilidade ou segurança. Por exemplo, se uma alteração de código resultar em um aumento significativo na latência do software, sobrecarga de processamento e assim por diante.
Esses tipos de regressão podem passar despercebidas. Nosso foco aqui neste artigo são as regressões funcionais. Então, vamos falar sobre como o teste de unidade pode ajudar a detectar regressões.
📍 Qual é o papel dos testes de unidade para ajudar a detectar regressões?
Muitas vezes acabamos subestimando o papel dos testes de unidade. Mas eles são essenciais principalmente para evitar regressões. Vejamos um exemplo, primeiro de forma didática. Observe a classe Phone
abaixo:
A classe é bem simples e busca validar um telefone com os critérios nas estruturas condicionais, mas vamos adicionar uma nova regra para a classe Telefone 👇:
O desenvolvedor então executa os testes e tudo parece bem. Além disso, após a nova regra ser adicionada ao código, o desenvolvedor implementa uma refatoração simples, veja como ficou:
Agora, vamos criar um novo teste para verificar se a nova validação se comporta conforme o esperado:
Naturalmente, espera-se que o desenvolvedor execute todos os testes ou uma ferramenta de automação faça isso, por exemplo, após um push para o repositório. Quando o programador executa o conjunte de teste, nota que um teste que já foi escrito por outro desenvolvedor falhou, veja abaixo o teste que não passou:
Para encontrar o problema, a maneira mais fácil e talvez mais rápida é depurar, mas podemos fazer um exercício lógico aqui. Antes da refatoração, cada estrutura condicional continha seus próprios retornos. Mas agora temos uma única estrutura condicional com o operador lógico && (AND) que aparece duas vezes!
return phone.All(char.IsDigit) && phone.Length > 11 && phone.Length < 13;
Observe um ponto importante, se a string contiver apenas números, for maior que 11 e menor que 13 o retorno é true, mas e se a string for igual a 11 e 13? O comportamento anterior do código, que é o comportamento esperado pelo cliente e declarado nos requisitos, considerava válida uma string com comprimento de 11 e 13 caracteres. Mas não mais! O comportamento da classe foi quebrado. Então, o que isso nos mostra? Primeiro, o teste de unidade impediu uma regressão de funcionalidade!
Em segundo, se não existissem testes de unidade, você precisaria de um teste manual para poder detectar essa regressão.
Você entende mais claramente a importância do teste de unidade na luta contra a regressão de funcionalidade? Se o número de telefone do usuário contivesse 12 caracteres, tudo bem a validação seria aceita. Mas qualquer outro usuário que tivesse um número de telefone com 11 ou 13 caracteres não teria um número de telefone válido para o sistema.
Com testes de unidade, as regressões são fáceis de detectar, trazendo confiança aos desenvolvedores e à equipe de controle de qualidade. Agora, depois que o teste falhou, o desenvolvedor pode detectar o problema com segurança e corrigir a regressão:
O operador de igualdade declarado agora permite uma string com comprimento igual a 11 ou 13 caracteres. Você pode estar pensando: "Isso não se encaixaria como resistência à refatoração?" Não! Nesse caso, o comportamento final foi alterado, não por causa da refatoração, mas por causa da lógica incorreta implementada durante o processo. O comportamento observável foi alterado porque o desenvolvedor não tentou certificar-se dos requisitos.
Portanto, é claro que este é um caso em que os testes de unidade ajudaram a detectar uma regressão. Aqui está a definição de refatoração:
Refatorar significa alterar o código existente sem alterar seu comportamento observável. A intenção é melhorar as características não funcionais do código, como tornar o código mais fácil de ler e diminuir a complexidade geral do algoritmo.
Vamos agora entender quais estratégias podemos adotar para melhorar o alcance de nossos testes em regressões.
📍 Como aumentar as chances dos testes revelarem regressões?
Este pode ser um assunto profundo, mas é importante comentar. Afinal, como aumentar as chances dos testes revelarem regressões de funcionalidades já existentes? Bem, podemos tentar mencionar duas estratégias.
A primeira que considero essencial é identificar componentes que são essenciais e geralmente são responsáveis por validar e executar regras críticas para o sistema . Por que? Normalmente, nessas classes ou componentes, uma regressão da funcionalidade pode significar prejuízos incalculáveis ao negócio. Em grandes aplicações que seguem boas práticas e princípios, as regras de negócio residem no domínio. Podemos dizer que existem grandes validações e regras. Uma estratégia para aumentar as chances de encontrar regressões é focar em testar comportamentos dessas classes e componentes com foco em requisitos. Devemos lembrar que quanto mais código tivermos em um sistema, mais expostos estaremos ao risco de fazer alterações que regredirão o software a um estado de comportamento inaceitável, por isso é importante garantir que esses recursos sejam sempre protegidos com testes de qualidade. O grande problema? Normalmente, muitos desenvolvedores não têm o privilégio de trabalhar em sistemas que tenham um design de código e arquitetura aceitáveis e muitas validações estão em vários pontos do aplicativo. Eu mesmo experimentei isso.
Então, como ainda podemos usar testes de unidade para evitar regressões nesse cenário? Um livro que me ajudou e ainda ajuda é Refactoring Improving the Design of Existing Code de Martin Fowler, esse livro traz dicas essenciais para refatorar métodos, classes e componentes com segurança, e o melhor de tudo, usando testes!
A segunda estratégia que podemos adotar, analisar a complexidade do código. Por que? Quando a complexidade do código aumenta, as chances de regressões de funcionalidade também aumentam. As vezes o fluxo é fácil de entender, mas o acoplamento e métodos com nomes difíceis e grandes, dificultam para o desenvolvedor adicionar nova funcionalidade a uma classe já existente, podemos citar rapidamente outros fatores:
Interdependências: Em sistemas complexos, componentes e módulos de código tendem a ser mais interconectados e interdependentes. Alterações em uma parte do sistema podem causar efeitos indesejados e inesperados em outras partes.
Difícil de entender: código complexo é mais difícil de entender e analisar. Isso aumenta a probabilidade de os desenvolvedores introduzirem erros ou modificarem partes do código que afetam a funcionalidade existente.
Reutilização de código: A complexidade geralmente leva à reutilização de código, o que pode ser benéfico em termos de eficiência, mas também pode resultar em problemas quando as alterações em um componente são propagadas para outros componentes que o reutilizam.
Outro ponto a ser observado é que quanto mais complexa a lógica de negócios se torna e com mais fluxos, as chances de regressões também aumentam, pois fica mais difícil para os desenvolvedor entender e gerenciar as interações entre as diferentes partes do sistema. Não devemos esquecer que isso também aumenta a probabilidade de ocorrência de erros ou mal-entendidos, levando a mudanças não intencionais no comportamento do software.
Podemos fazer uma analogia bem sucinta para facilitar o entendimento do quão crítico é esse fator de complexidade.
Pense em um emaranhado de fios elétricos, onde cada fio representa para nós uma parte do código. Quando os fios estão bem organizados e identificados, fica fácil rastrear e solucionar problemas. No entanto, à medida que o emaranhado de fios aumenta em complexidade, torna-se cada vez mais difícil identificar qual fio está conectado a qual componente. Consequentemente, é mais provável que uma alteração em um fio afete outros componentes do sistema de maneira não intencional, levando a regressões. Como os eletricistas evitam regressões? Os eletricistas usam um multímetro para verificar se há uma conexão adequada entre dois pontos em um circuito. Esse tipo de teste é conhecido como teste de continuidade. Isso ajuda a identificar se um fio está conectado corretamente ou se há uma interrupção no circuito. Eles fazem isso para garantir que não existem regressões de outros componentes. Também podemos destacar que eles procuram ser organizados, mantendo cada fio o mais separado possível, e usam etiquetas para identificar diferentes fios.
Assim como os eletricistas, nós desenvolvedores temos testes unitários para verificar o comportamento do código, não precisamos deixar essa ferramenta de lado. O eletricista pode identificar falhas no circuito elétrico após adicionar um novo fio ou componente usando técnicas de teste, e se também criarmos testes, podemos evitar regressões! Mas é bom frisar que os testes devem ser abrangentes e eficazes, só assim podem ajudar a detectar e corrigir regressões rapidamente, aumentando a qualidade e confiabilidade do software. Possuir testes de unidade torna as regressões mais fáceis de entender e detectar pelos motivos listados abaixo:
Foco no comportamento esperado: testes de unidade escritos com foco no comportamento esperado do código, ao invés de sua implementação, ajudam a identificar desvios do comportamento desejado, o que pode sinalizar a presença de regressão.
Isolamento: os testes de unidade visam partes específicas do código, isolando a funcionalidade em teste. Isso facilita a identificação da causa raiz da regressão, pois provavelmente está relacionada ao código ou componente que está sendo testado.
Feedback rápido: os testes de unidade fornecem feedback rápido sobre o comportamento recém-escrito ou novo recurso. Quando ocorre a regressão, os desenvolvedores podem identificá-la rapidamente e corrigi-la antes que se torne um problema maior.
O objetivo dos testes é ajudar o sistema a crescer de forma saudável e rápida, trazendo confiança e segurança para todos os envolvidos. Portanto, crie sempre o hábito de criar testes de qualidade, especialmente para funcionalidades críticas para os negócios!
📍 Benefícios de se evitar regressões!
Vejamos rapidamente benefícios importantes e que são visíveis rapidamente quando colocamos em prática testes que evitam regressões:
Simplificação de modificações: Com uma ampla gama de testes de unidade, os desenvolvedores podem realizar alterações e refatorações de código com maior confiança, sabendo que os testes os notificarão se alguma funcionalidade existente for afetada.
Reforço da confiabilidade do produto: uma série abrangente de testes de unidade bem-sucedidos aumenta a credibilidade do software, tanto para desenvolvedores quanto para as partes interessadas, provando que o produto funciona conforme o esperado.
Manutenibilidade aprimorada: testes de unidade escritos e atualizados corretamente simplificam o processo de manutenção de software, pois ajudam a identificar rapidamente as consequências das alterações de código e garantem que a funcionalidade não seja prejudicada.
Facilitação da integração: testes de unidade de qualidade facilitam a incorporação de novos recursos ou componentes, garantindo que as modificações não afetem negativamente o software e seu comportamento observável.
Poderia listar outros, mas a leitura já está bem grande, então vou parar por aqui! 😅
📍 Conclusão
Em conclusão, manter o valor do teste é crucial para garantir a melhoria contínua e a estabilidade dos aplicativos de software. As regressões representam uma ameaça significativa à qualidade do software, pois podem reintroduzir problemas resolvidos anteriormente ou criar novos. Para manter os testes valiosos e evitar regressões, é essencial adotar práticas como revisões completas de código, testes regulares, suítes de testes automatizadas e comunicação eficaz entre os membros da equipe e sempre tentar criar testes mais abrangentes que procurem por bugs. Fique alerta!
No próximo post vamos falar sobre um assunto que às vezes passa despercebido, mas que também pode causar regressões e afetar os testes, falsos negativos.
Muito obrigado por ler até o final, espero que as dicas possam ajudar! Por favor, curta e compartilhe! Até o próximo artigo! 😄🖐️